Troubleshooting
Diagnose and fix common Earl issues — keychain errors, template validation, MCP connection, and more.
Start with earl doctor. It runs all the checks in one pass and tells you exactly what's wrong and what to do. The problems below are what you hit when doctor either passes or when the fix needs more than one step.
earl doctor fails
earl doctor reports three severity levels: ok, warning, and error. Errors cause a non-zero exit. Warnings are things to fix eventually but won't block basic operation.
Binary not found. If earl isn't on your PATH, doctor won't even run. On macOS/Linux, verify the install:
which earl
earl --versionIf the binary is missing, reinstall:
curl -fsSL https://raw.githubusercontent.com/mathematic-inc/earl/main/scripts/install.sh | bashKeychain unavailable. Doctor tries to access the OS keychain when checking for required secrets. If it reports a keychain error, see the Linux keychain section below.
No templates found. This is a warning, not an error. Doctor looks in ./templates/ (relative to cwd) and ~/.config/earl/templates/. If both are empty, import a template:
earl templates import https://raw.githubusercontent.com/mathematic-inc/earl/main/examples/github.hclTemplate validation failed. Doctor runs earl templates validate internally. If it errors here, run validation directly to see the specific file and line:
earl templates validateJWT configured but no policy rules. Doctor warns when [auth.jwt] is set but no [[policy]] entries exist. With JWT auth active and no rules, every MCP tool call is denied. Add at least one allow rule.
OAuth profile missing or invalid. If templates reference an OAuth profile that isn't in config.toml, or the profile is missing required fields (like token_url), doctor reports an error. Fix the profile in ~/.config/earl/config.toml and re-run doctor.
macOS keychain dialog keeps appearing
Cause: macOS shows a system dialog the first time any process reads or writes a keychain entry. If you click "Deny" or "Allow" (one-time), the dialog appears again on the next access.
Fix: Click "Always Allow" when the dialog appears. This grants Earl persistent access to that keychain item without prompting on subsequent runs.
If you clicked "Deny" and the dialog has stopped appearing but earl call fails with a keychain error, the entry may have been blocked. Open Keychain Access, find the Earl entry, and delete the explicit deny. Then run earl secrets set again to recreate the entry and click "Always Allow" this time.
Linux: earl secrets set exits with a keyring error
Cause: Earl uses Secret Service via libsecret on Linux. Secret Service requires a running daemon — either gnome-keyring-daemon, kwallet, or KeePassXC with the Secret Service integration enabled. If none is running, libsecret has nowhere to write.
Fix for GNOME / gnome-keyring:
# Check if the daemon is running
ps aux | grep gnome-keyring
# Start it manually if not
eval $(gnome-keyring-daemon --start --components=secrets)
export DBUS_SESSION_BUS_ADDRESSFor headless or server environments, you may need to unlock the default keyring programmatically. The secret-tool utility (from libsecret-tools) can test access:
echo -n "test" | secret-tool store --label="test" key testkeyFix for KDE / kwallet:
# Check kwallet status
qdbus org.kde.kwalletd5 /modules/kwalletd5 isEnabledFix for KeePassXC:
Open KeePassXC settings, navigate to Secret Service Integration, enable it, and restart KeePassXC. This exposes the Secret Service D-Bus interface that libsecret expects.
Fix for headless CI/server environments:
For automated environments where no user session exists, use the --stdin flag to pipe secrets directly rather than storing them in a keychain:
echo "$GITHUB_TOKEN" | earl secrets set github.token --stdinearl call fails with an SSRF error
Cause: Earl blocks all requests to private and loopback IP ranges in security/ssrf.rs. This runs on every outbound request and cannot be disabled. The blocked ranges include:
127.0.0.0/8— loopback10.0.0.0/8,172.16.0.0/12,192.168.0.0/16— RFC 1918 private169.254.0.0/16— link-local (includes the AWS metadata endpoint169.254.169.254)- And several others; see Hardening for the full table.
If a hostname resolves to a blocked IP, the request is rejected before any connection is attempted.
Fix: There is no config option to bypass SSRF blocking. If you need to reach an internal service, consider whether Earl is the right tool for that call — Earl is designed for external API integrations.
If you need to allow specific external hosts for other reasons, [[network.allow]] entries in config.toml restrict which hosts Earl can reach, but they don't override SSRF blocking for private IPs. They operate at the URL layer, not the IP layer.
Template validation errors
"protocol mismatch" or schema error. The protocol field inside an operation { } block determines which fields are valid. Each protocol uses a flat or nested structure:
# HTTP: fields are flat in the operation block
operation {
protocol = "http"
method = "GET"
url = "https://api.example.com/endpoint"
}
# SQL: fields are nested in a sql { } block
operation {
protocol = "sql"
sql {
query = "SELECT * FROM users WHERE id = $1"
params = ["{{ args.id }}"]
}
}Mixing up flat vs. nested structure causes a parse error.
"secret not in annotations.secrets". The key in auth.secret must be listed in annotations.secrets. Earl validates this when the template loads:
annotations {
secrets = ["github.token"] # must list it here
}
operation {
auth {
kind = "bearer"
secret = "github.token" # and reference it here
}
}HCL parse error with Jinja expressions. HCL is parsed before Jinja template rendering. This means {{ }} expressions inside HCL must be valid HCL syntax first. The most common mistake:
# Wrong — bare Jinja expression is invalid HCL in a params array
params = [{{ args.limit }}]
# Correct — wrap in quotes; Earl coerces the type before the query runs
params = ["{{ args.limit }}"]Pure string expressions like "{{ args.limit }}" are automatically type-coerced by Earl's render_string_value function, so a numeric value comes through as a number even though the template uses a quoted string.
MCP tools don't appear in the agent
Cause: MCP tools are not live-reloaded. They're registered when the agent starts and don't change until restart.
Fix: After adding or updating Earl's MCP config, restart your agent. There's no way around this — it's how MCP works. In the current session, you can still call Earl through the terminal:
earl call --yes --json provider.command --param valueAfter restarting, templates appear as native tools.
If tools still don't appear after restart, check that the MCP config is correct:
{
"mcpServers": {
"earl": {
"command": "earl",
"args": ["mcp", "stdio"]
}
}
}Run earl doctor from the same directory the agent uses as its working directory — template discovery is relative to cwd.
gRPC reflection fails
Cause: Earl uses gRPC reflection v1. Some servers only implement v1alpha. When v1 fails, Earl can't discover the service definition and will error.
Fix 1: Check which reflection version the server supports. If the server supports v1alpha but not v1, there's no automatic fallback — you need to use a descriptor file.
Fix 2: Use a pre-compiled descriptor set file instead of reflection. Generate one with protoc:
protoc --descriptor_set_out=service.pb \
--include_imports \
--proto_path=. \
your_service.protoThen reference it in the template:
operation {
protocol = "grpc"
url = "https://api.example.com"
grpc {
service = "example.v1.ExampleService"
method = "GetItem"
descriptor_set_file = "./service.pb"
}
}TOCTOU note: Earl's gRPC implementation pins the endpoint to the IP resolved at connection time for SSRF protection. This can break TLS when using gRPC reflection against servers that expect SNI. If you're testing locally and hitting TLS errors, use http:// instead of https:// or use a descriptor file.
OAuth flow: browser doesn't open
Cause: auth_code_pkce opens the system browser. On headless systems, or when the browser is unavailable, the open command silently fails.
Fix: Earl prints the authorization URL to the terminal. Copy it and open it in any browser:
Open the following URL to authorize:
https://accounts.example.com/oauth/authorize?client_id=...&code_challenge=...After authorizing, the browser is redirected to the callback URL. Earl's local server is listening for it. If the browser opens on a different machine, you need to be able to reach http://localhost:8976/callback (or whatever redirect_url is set to) from the machine that completed the auth.
For headless / agent environments: Use device_code instead of auth_code_pkce. Earl prints a short code and a URL; you enter the code from any device:
[auth.profiles.myprofile]
flow = "device_code"
client_id = "your-client-id"
issuer = "https://accounts.example.com"
scopes = ["repo"]earl auth login myprofile
# Output: Visit https://accounts.example.com/activate and enter code: ABCD-1234Secret value appears in output
Earl's redactor scans all output before returning it to the caller. Any secret used during the call — raw value, plus its base64, hex, and URL-encoded forms — is replaced with [REDACTED].
If a secret value is still appearing:
-
Verify the secret is actually declared in
annotations.secretsfor the command. Secrets not declared there are not tracked by the redactor for that call. -
The redactor covers raw, base64, hex, and URL-encoded forms. It doesn't cover every possible encoding. A value that's been transformed in an unusual way (fragmented, encrypted, custom-encoded) won't be caught.
-
Check whether the secret is appearing in a field that's being returned through a Jinja expression. The
result.outputtemplate has no access tosecrets.*— you cannot accidentally expand a secret that way. But if an API response happens to include a secret value in its JSON body (e.g., an error message that echoes back the token), the redactor should catch it.
If the redactor is missing a case, the most direct fix is to not return the field that contains the sensitive data. Use result.extract with a JSON pointer or regex to pull out only the fields you need, rather than returning the full response.
--yes and --json flags aren't working
Cause: These flags must come before provider.command. Anything after the command name is treated as template parameters, not flags.
# Wrong — --yes is parsed as a template parameter, not a flag
earl call github.create_issue --yes --owner myorg --repo myrepo
# Correct
earl call --yes github.create_issue --owner myorg --repo myrepo
# Both flags before the command
earl call --yes --json github.create_issue --owner myorg --repo myrepoThis applies to --env as well:
earl call --env staging github.search_repos --query "rust"