earl

Configuration

Complete reference for ~/.config/earl/config.toml — every section, field, and default value.

~/.config/earl/config.toml is optional. Earl works without it. When the file exists, it controls template discovery, authentication, network egress, sandbox limits, environments, and access policy. Every section has sensible defaults, so you only need to configure what you're changing.

Earl creates the config directory (~/.config/earl/) on first run if it doesn't exist.

Minimal example

A config file that only restricts outbound network access:

[[network.allow]]
scheme      = "https"
host        = "api.github.com"
port        = 443
path_prefix = "/"

[[network.allow]]
scheme      = "https"
host        = "api.stripe.com"
port        = 443
path_prefix = "/"

Full production example

# Network egress: allow only specific hosts
[[network.allow]]
scheme      = "https"
host        = "api.github.com"
port        = 443
path_prefix = "/"

[[network.allow]]
scheme      = "https"
host        = "api.stripe.com"
port        = 443
path_prefix = "/"

# JWT validation for MCP HTTP transport
[auth.jwt]
audience           = "https://api.yourcompany.com"
oidc_discovery_url = "https://accounts.yourcompany.com/.well-known/openid-configuration"

# OAuth2 profile for GitHub browser-based auth
[auth.profiles.github]
flow         = "auth_code_pkce"
client_id    = "your-client-id"
issuer       = "https://github.com"
scopes       = ["repo", "read:org"]
redirect_url = "http://localhost:8080/callback"

# Sandbox: tighter limits than the per-command defaults
[sandbox]
bash_allow_network    = false
bash_max_time_ms      = 10000
bash_max_output_bytes = 524288
sql_force_read_only   = true
sql_max_rows          = 50

# Default environment
[environments]
default = "production"

# Policy rules for MCP HTTP (only evaluated when auth.jwt is configured)
[[policy]]
subjects = ["user:*"]
tools    = ["github.*"]
modes    = ["read"]
effect   = "allow"

[[policy]]
subjects = ["*"]
tools    = ["github.delete_repo"]
modes    = ["write"]
effect   = "deny"

# Search: remote semantic search backend (optional)
[search.remote]
enabled          = true
base_url         = "https://search.yourcompany.com"
api_key_secret   = "search.api_key"
embeddings_path  = "/embeddings"
rerank_path      = "/rerank"
openai_compatible = true
timeout_ms       = 10000

Controls how Earl discovers and ranks templates.

[search]
top_k    = 40
rerank_k = 10
FieldTypeDefaultDescription
top_kinteger40Number of candidates to retrieve before reranking
rerank_kinteger10Number of results to return after reranking

[search.local]

Local embedding and reranking models. Earl bundles models via fastembed; no external service needed.

[search.local]
embedding_model  = "BGESmallENV15Q"
reranker_model   = "JINARerankerV1TurboEn"
FieldTypeDefaultDescription
embedding_modelstring"BGESmallENV15Q"fastembed embedding model identifier
reranker_modelstring"JINARerankerV1TurboEn"fastembed reranker model identifier

[search.remote]

Optional remote semantic search backend (OpenAI-compatible API). When enabled = false (the default), Earl uses only local models.

[search.remote]
enabled           = true
base_url          = "https://search.example.com"
api_key_secret    = "search.api_key"
embeddings_path   = "/embeddings"
rerank_path       = "/rerank"
openai_compatible = true
timeout_ms        = 10000
FieldTypeDefaultDescription
enabledbooleanfalseEnable remote search
base_urlstringBase URL of the remote search service
api_key_secretstringKeychain key name containing the API key
embeddings_pathstring"/embeddings"Path for the embeddings endpoint
rerank_pathstring"/rerank"Path for the reranking endpoint
openai_compatiblebooleantrueUse OpenAI-compatible request format
timeout_msinteger10000Request timeout in milliseconds

The api_key_secret value is a keychain key name, not the key itself. Store the value first:

earl secrets set search.api_key

[auth.jwt]

JWT validation for the MCP HTTP transport. Earl won't start earl mcp http without either this section configured or --allow-unauthenticated passed. The two approaches are mutually exclusive.

Two ways to configure JWT validation:

OIDC discovery (recommended) — Earl fetches the JWKS endpoint from the discovery document automatically:

[auth.jwt]
audience           = "https://api.yourcompany.com"
oidc_discovery_url = "https://accounts.yourcompany.com/.well-known/openid-configuration"

Manual JWKS — use this when there's no OIDC discovery endpoint:

[auth.jwt]
audience                   = "https://api.yourcompany.com"
issuer                     = "https://accounts.yourcompany.com"
jwks_uri                   = "https://accounts.yourcompany.com/.well-known/jwks.json"
algorithms                 = ["RS256"]
clock_skew_seconds         = 30
jwks_cache_max_age_seconds = 900

oidc_discovery_url and issuer/jwks_uri are mutually exclusive. Earl errors if you set both.

FieldTypeRequiredDefaultDescription
audiencestringYesExpected aud claim in incoming tokens
oidc_discovery_urlstringIf no issuer/jwks_uriOIDC discovery URL; resolves issuer and JWKS URI automatically
issuerstringIf no oidc_discovery_urlExpected iss claim
jwks_uristringIf no oidc_discovery_urlJWKS endpoint URL
algorithmsstring[]No["RS256"]Allowed signing algorithms
clock_skew_secondsintegerNo30Clock skew tolerance (max effective: 300)
jwks_cache_max_age_secondsintegerNo900JWKS cache TTL in seconds

Supported algorithms: RS256, RS384, RS512, ES256, ES384, ES512, PS256, PS384, PS512, EdDSA.

Incoming tokens must carry exp, sub, iss, and aud claims. The sub claim is what [[policy]] rules match against.


[auth.profiles.*]

OAuth2 profiles used by templates with auth { kind = "o_auth2_profile" profile = "..." }. Add one [auth.profiles.<name>] section per profile.

Three flows are available. Which one fits depends on whether a human needs to be in the loop.

Auth Code + PKCE

Opens a browser. A human must authorize before the token is issued. Not suitable for fully unattended flows.

[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 authorization URL in the default browser. A local server listens at the redirect_url for the authorization code. The default redirect is http://127.0.0.1:8976/callback if redirect_url is omitted.

Device Code

Works without a browser on the machine running Earl. Earl prints a short code and a URL; a human enters the code from any device. Earl polls until authorization completes. Agent-compatible.

[auth.profiles.device]
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"]

If your provider supports OIDC discovery, set issuer instead of listing endpoint URLs:

[auth.profiles.device]
flow      = "device_code"
client_id = "your-client-id"
issuer    = "https://accounts.example.com"
scopes    = ["repo"]

Client Credentials

No human involved. Used for M2M flows and service accounts. earl auth login completes immediately.

[auth.profiles.service]
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 keychain key name. Store the value first:

earl secrets set myservice.client_secret

OAuth profile fields

FieldTypeRequiredDescription
flowstringYesauth_code_pkce, device_code, or client_credentials
client_idstringYesOAuth2 client ID
client_secret_keystringclient_credentials onlyKeychain key name for the client secret
issuerstringIf no endpoint URLsOIDC issuer; Earl fetches discovery document to find endpoints
authorization_urlstringauth_code_pkce onlyAuthorization endpoint (if no issuer)
token_urlstringIf no issuerToken endpoint
device_authorization_urlstringdevice_code onlyDevice authorization endpoint (if no issuer)
redirect_urlstringNoCallback URL for auth_code_pkce; defaults to http://127.0.0.1:8976/callback
scopesstring[]NoRequested OAuth2 scopes
use_auth_request_bodybooleanNoSend client credentials in the request body instead of the Authorization header

[network]

Top-level network settings that apply to all outbound requests.

[network]
allow_private_ips = false
FieldTypeDefaultDescription
allow_private_ipsbooleanfalseAllow requests to RFC 1918 private addresses and loopback. Enable for homelab or self-hosted services. Cloud metadata endpoints remain blocked.

Set allow_private_ips = true when Earl needs to reach local services such as Home Assistant, Frigate, OPNsense, or Gitea running on your network. Cloud metadata endpoints (169.254.169.254, fd00:ec2::254, etc.) and other hazardous ranges remain blocked regardless. See Hardening for the full list of ranges that allow_private_ips affects.

[[network.allow]]

Restricts which hosts Earl can contact. Without any entries, Earl can reach any public IP (SSRF blocking of private ranges still applies regardless). With entries, any request to a host not matching at least one rule is rejected.

Each entry is a separate [[network.allow]] block with four fields, all required:

[[network.allow]]
scheme      = "https"
host        = "api.github.com"
port        = 443
path_prefix = "/"

[[network.allow]]
scheme      = "https"
host        = "api.stripe.com"
port        = 443
path_prefix = "/"
FieldTypeDescription
schemestringURL scheme to match ("https" or "http")
hoststringExact hostname to allow
portintegerPort number to allow
path_prefixstringURL path prefix; use "/" to allow all paths

path_prefix = "/v2/" allows only paths starting with /v2/. Use "/" to allow the full host.

This is separate from SSRF blocking, which operates at the IP layer. The network allowlist operates at the URL layer and restricts outbound access to specific destinations. See Hardening for the full list of SSRF-blocked ranges.

[network.proxy_profiles.*]

Named proxy configurations, referenced by templates using transport { proxy_profile = "..." }.

[network.proxy_profiles.corp]
url = "http://proxy.corp.example.com:8080"
FieldTypeRequiredDescription
urlstringYesProxy URL including scheme and port

[sandbox]

Global defaults for Bash and SQL sandbox limits. Per-command sandbox { } blocks in templates override these for individual commands.

[sandbox]
bash_allow_network    = false
bash_max_time_ms      = 30000
bash_max_output_bytes = 1048576
bash_max_memory_bytes = 134217728
bash_max_cpu_time_ms  = 30000
sql_force_read_only   = true
sql_max_rows          = 100
sql_connection_allowlist = []
FieldTypeDefaultDescription
bash_allow_networkbooleanfalseAllow outbound network access from Bash scripts (SSRF blocking still applies)
bash_max_time_msintegernoneKill Bash processes after this many milliseconds
bash_max_output_bytesintegernoneTruncate Bash stdout + stderr at this byte count
bash_max_memory_bytesintegernoneMemory limit for Bash sandbox processes
bash_max_cpu_time_msintegernoneCPU time limit for Bash sandbox processes
sql_force_read_onlybooleantrueOpen SQL connections in read-only transactions
sql_max_rowsintegernoneCap rows returned by SQL queries
sql_connection_allowliststring[][]Keychain key names whose connection strings are permitted

sql_force_read_only = true is the default. Set it to false only if you have SQL commands that genuinely need to write. The setting opens connections in a read-only transaction at the database level — write operations fail at the driver, not just at the application.


[environments]

Sets the default environment when no --env flag is passed to earl call.

[environments]
default = "production"
FieldTypeDefaultDescription
defaultstringnoneDefault environment name

The --env flag on earl call overrides this. This config value overrides the default set inside a template's environments { } block. For how environments work in templates, see Environments.


[[policy]]

Access control rules for the MCP HTTP transport. Only evaluated when [auth.jwt] is configured. Each rule is a separate [[policy]] block.

# Read-only GitHub access for all authenticated users
[[policy]]
subjects = ["user:*"]
tools    = ["github.*"]
modes    = ["read"]
effect   = "allow"

# Block destructive GitHub operations for everyone
[[policy]]
subjects = ["*"]
tools    = ["github.delete_repo"]
modes    = ["write"]
effect   = "deny"
FieldTypeRequiredDescription
subjectsstring[]YesCaller identity patterns matched against the JWT sub claim
toolsstring[]YesTool name patterns (provider.command format)
modesstring[]No"read", "write", or both. Omit to match any mode.
effectstringYes"allow" or "deny"

Subject patterns:

PatternMatches
user:aliceJWT sub equals alice
user:*Any sub value
group:adminsCallers with admins in their groups claim
*Any authenticated caller

Tool patterns: * matches within a single segment.

PatternMatches
github.*All github commands
*.delete_*Any delete command across all providers
*Any tool

Evaluation: deny-overrides. One deny beats any number of allows. If no rule matches, the call is rejected. There is no implicit allow.

If [auth.jwt] is configured but no [[policy]] rules exist, all tool calls are denied. earl doctor warns about this.

See Policy Engine for subject patterns, tool glob syntax, and the evaluation model.

On this page