Secrets & Authentication
How Earl stores secrets in the OS keychain, references them in templates, and manages OAuth2 tokens.
Secrets in Earl never leave the OS keychain. When an agent calls earl call github.get_repo, Earl pulls github.token from the keychain right before executing the request. The token is not in the tool arguments, not in the MCP tool description, and not returned to the caller afterward.
OS keychain storage
Earl uses whatever credential store the OS provides:
- macOS: System Keychain (Security.framework, native)
- Linux: Secret Service / libsecret (
gnome-keyring,kwallet, orkeepassxcwith Secret Service enabled) - Windows: Windows Credential Manager
Values are never written to disk in plaintext.
Commands
# Store a secret — prompts for the value interactively (not echoed)
earl secrets set github.token
# Store from stdin — useful in scripts or CI
echo "ghp_xxxx" | earl secrets set github.token --stdin
# Show metadata (key name and timestamps, never the value)
earl secrets get github.token
# List all stored keys
earl secrets list
# Remove a secret
earl secrets delete github.tokenearl secrets get shows key name, creation time, and last updated time. The value field always shows [REDACTED]. There is no flag to print the raw value.
macOS: The first time Earl accesses the keychain in a session, macOS may show a system dialog asking for permission. Click "Always Allow" to avoid repeated prompts.
Linux: A Secret Service daemon must be running. If none is active, earl secrets set exits with a keyring error. See Troubleshooting.
Safety model
A few things Earl deliberately does not do:
earl secrets setstores the value directly in the OS credential store. It does not write a file.- Templates hold key names, not values.
"github.token"in a template is a reference, not the token. result.outputhas no access tosecrets.*. Writing{{ secrets.api_key }}in an output block causes a template render error — the secrets namespace is not available in that context.- If a secret value appears in API response data anyway, Earl's redactor replaces it with
[REDACTED]before returning output. The check covers the raw value plus its base64, hex, and URL-encoded variants.
Referencing secrets in templates
Declare which secrets a command needs in annotations.secrets, then reference the key name in the auth block:
annotations {
secrets = ["github.token"]
}
operation {
protocol = "http"
method = "GET"
url = "https://api.github.com/user/repos"
auth {
kind = "bearer"
secret = "github.token"
}
}The key in auth.secret must appear in annotations.secrets. Earl validates this when the template loads — if it doesn't match, the template won't run.
Auth block kinds
Bearer token:
auth {
kind = "bearer"
secret = "github.token"
}API key:
auth {
kind = "api_key"
location = "header" # header, query, or cookie
name = "X-API-Key"
secret = "stripe.api_key"
}HTTP Basic:
auth {
kind = "basic"
username = "myuser"
password_secret = "registry.password"
}OAuth2 profile:
auth {
kind = "o_auth2_profile"
profile = "myprofile"
}References an OAuth2 profile defined in config.toml. See OAuth2 authentication below.
OAuth2 authentication
OAuth2 tokens go through the same keychain as static secrets. Earl handles token fetch, refresh, and expiry automatically — you don't manage token files.
Three flows are available. Which one to use depends on whether a human needs to be involved.
Configuring a profile
Add a profile in ~/.config/earl/config.toml under [auth.profiles.<name>]. The name you choose here is what you pass to earl auth login and to auth.profile in templates.
Auth Code + PKCE — requires a browser. An agent can start the flow, but a human has to authorize in the browser:
[auth.profiles.myprofile]
flow = "auth_code_pkce"
client_id = "your-client-id"
issuer = "https://accounts.example.com"
scopes = ["read", "write"]
redirect_url = "http://localhost:8080/callback"Earl opens the browser, the user authorizes, and a local callback server receives the code. The default callback is http://127.0.0.1:8976/callback.
Device Code — agent-compatible. Earl prints a URL and a short code. A human enters the code at that URL on any device. Earl polls for completion:
[auth.profiles.myprofile]
flow = "device_code"
client_id = "your-client-id"
device_authorization_url = "https://accounts.example.com/device/code"
token_url = "https://accounts.example.com/token"
scopes = ["repo", "gist"]Client Credentials — no human needed. Used for service accounts and M2M flows. earl auth login completes immediately:
[auth.profiles.myprofile]
flow = "client_credentials"
client_id = "your-client-id"
client_secret_key = "myservice.client_secret"
token_url = "https://accounts.example.com/token"
scopes = ["api.read"]client_secret_key is a key name in the OS keychain:
earl secrets set myservice.client_secretIf your identity provider supports OpenID Connect, you can set issuer instead of listing endpoint URLs. Earl fetches <issuer>/.well-known/openid-configuration to find token_url, authorization_url, and device_authorization_url.
Auth commands
earl auth login myprofile # start the OAuth flow
earl auth status myprofile # check token expiry
earl auth refresh myprofile # force a refresh
earl auth logout myprofile # remove the stored tokenUsing a profile in a template
operation {
auth {
kind = "o_auth2_profile"
profile = "myprofile"
}
}Earl loads the stored token, checks expiry, refreshes if needed, and sends the access token as a Bearer header.
Token storage
Tokens are stored under oauth2.<profile>.token in the OS keychain. The payload includes access_token, refresh_token (if the provider returned one), expiry, and granted scopes. Earl treats tokens as expired 30 seconds before their stated expiry to account for clock skew.
Endpoint security
OAuth endpoint URLs go through the same SSRF validation as any other outbound request: they must use https (or http to a loopback address) and must not resolve to private IP ranges. See Hardening for the full list of blocked ranges.
External secret managers
For team deployments, secrets can live in 1Password, HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, or Azure Key Vault instead of the OS keychain. See External Secrets for setup and reference syntax.