earl

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 --version

If the binary is missing, reinstall:

curl -fsSL https://raw.githubusercontent.com/mathematic-inc/earl/main/scripts/install.sh | bash

Keychain 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.hcl

Template validation failed. Doctor runs earl templates validate internally. If it errors here, run validation directly to see the specific file and line:

earl templates validate

JWT 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_ADDRESS

For 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 testkey

Fix for KDE / kwallet:

# Check kwallet status
qdbus org.kde.kwalletd5 /modules/kwalletd5 isEnabled

Fix 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 --stdin

earl 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 — loopback
  • 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 — RFC 1918 private
  • 169.254.0.0/16 — link-local (includes the AWS metadata endpoint 169.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 value

After 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.proto

Then 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-1234

Secret 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:

  1. Verify the secret is actually declared in annotations.secrets for the command. Secrets not declared there are not tracked by the redactor for that call.

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

  3. Check whether the secret is appearing in a field that's being returned through a Jinja expression. The result.output template has no access to secrets.* — 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 myrepo

This applies to --env as well:

earl call --env staging github.search_repos --query "rust"

On this page