earl

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, or keepassxc with 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.token

earl 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 set stores 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.output has no access to secrets.*. 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_secret

If 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 token

Using 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.

On this page