Skip to content

Troubleshooting

The errors most users hit fall into a few buckets. Each one below is the literal message you’ll see, plus what it means and the fix.

A command tried to read a path that’s outside every read grant in the manifest. The path will be in the error.

Fixes, in increasing order of permissiveness:

  1. Add an explicit grant. If the agent should be allowed to read this path, add it under [capabilities.allow.read.paths] — or accept the read prompt next time it fires.
  2. Toggle a relevant bundle. Most tool-config dirs (~/.cargo, ~/.gitconfig, etc.) come for free with the matching [sandbox.os] command_tools bundle entry — command_tools = ["cargo"] adds the cargo config dirs without you naming each one.
  3. Loosen the baseline. [sandbox.os] command_fs = "permissive" adds $HOME read-only to every command’s baseline. Use sparingly — secrets stay masked but every other config dir becomes visible to every command.

Same shape as read denials but stricter — write grants are always explicit. The agent should never be writing to a path the user didn’t grant; if you see this, either add the path under [capabilities.allow.write.paths] or refuse the operation.

The command pattern matched nothing in [capabilities.allow.commands.patterns], AND runtime expansion is off. With expansion on (the default), you’d have seen a prompt instead. Either:

  • Turn expansion on ([security] allow_runtime_expansion = true, the default).
  • Add the pattern explicitly to the allowlist.

The legacy form of sandbox denied command. Same fix.

Connection refused from inside the sandbox

Section titled “Connection refused from inside the sandbox”

A command tried to reach a host the network allowlist doesn’t cover. The sandbox’s MITM proxy refuses the connection and the command sees a TCP-level error (often connection refused or network is unreachable — varies by tool).

Fix:

  • If a curated bundle owns the tool, check that the bundle has the host in its default_domains. The bundle docs at sandbox.os.command_tools list each bundle’s defaults.

  • For a one-off host, add a per-command entry under [capabilities.allow.commands] patterns:

    [capabilities.allow.commands]
    default_domains = ["registry.npmjs.org"]
    patterns = [
    "npm install:*",
    { command = "curl:*", domains = ["api.example.com"] },
    ]
  • If you want a bundle’s curated allowlist plus extra domains, use the expanded form: { tool = "git", domains = ["my-gitea.example.com"], default_domains = true }default_domains = true keeps the bundle’s curated github allowlist alongside the override.

SSL_ERROR_BAD_CERT_DOMAIN / cert-chain errors

Section titled “SSL_ERROR_BAD_CERT_DOMAIN / cert-chain errors”

The MITM proxy is signing TLS with a per-session CA. Tools that consult a system trust store will see this CA as untrusted unless they’re configured to trust it. Most tool bundles handle this automatically (cargo/bun/npm pick up the right env var). For a tool that doesn’t, the env-var passthrough is SSL_CERT_FILE pointing at the session CA — let us know if you hit a tool the bundles don’t cover and we’ll add it.

The on-disk seal.toml doesn’t match the signed-manifest hash from the last session. Usually means you (or a tool) hand-edited the file outside Seal. Two paths:

  • Accept the new manifest. Re-launch seal; the startup flow shows you the diff and asks you to re-approve. After approval, the new content + its hash become the session’s source of truth.
  • Revert. If the edit wasn’t intentional, restore seal.toml from VCS — the hash will line up and the next session will skip the approval step.

This check is per-session, per-project. Each project’s seal.toml has its own signature; editing one doesn’t affect others.

Manifest approval prompt fires every session

Section titled “Manifest approval prompt fires every session”

Default behavior — every fresh session re-validates the signature before any agent traffic. If you find the prompt annoying, [cli] always_approve = true in ~/.config/seal/config.toml is the override (but you’ll lose the “hey, the manifest changed under you” safety net).

401 invalid x-api-key (Anthropic, API-key path)

Section titled “401 invalid x-api-key (Anthropic, API-key path)”

ANTHROPIC_API_KEY is unset or the key has been revoked. Check echo $ANTHROPIC_API_KEY resolves to the key you expect; rotate the key in the Anthropic dashboard if you think it’s been leaked.

401 invalid x-api-key (Anthropic, OAuth path)

Section titled “401 invalid x-api-key (Anthropic, OAuth path)”

Your CLAUDE_CODE_OAUTH_TOKEN expired or was revoked. Generate a fresh one by running claude setup-token on a machine with Claude Code installed and re-export it. If claude setup-token itself fails, check that your Claude subscription is active and you can sign in to claude.ai in a browser. Full instructions: https://code.claude.com/docs/en/authentication#generate-a-long-lived-token.

You picked provider = "openrouter" but didn’t set the env var. Export OPENROUTER_API_KEY=sk-or-v1-... and re-launch.

The name field references a model the provider doesn’t expose to your account. Common causes:

  • Anthropic: claude-haiku-4-5 requires an account tier that has access; default to claude-sonnet-4-6 if you’re not sure.
  • OpenRouter: vendor prefix is required — claude-sonnet-4-6 won’t work; use anthropic/claude-sonnet-4-6.

”Allow always” doesn’t stop the prompt

Section titled “”Allow always” doesn’t stop the prompt”

You accepted Allow always for a pattern, then a slightly different invocation immediately prompts again — same command, different args.

The daemon infers a pattern from the first invocation; if you accepted cargo nextest run -p seal-policy:* and the agent later runs cargo nextest run -p seal-daemon, the new arg doesn’t match. Re-prompt at the new call, press Left to broaden (cargo nextest run:* or cargo nextest:*), and Allow always at the wider level — or edit the manifest entry directly. See Specificity scores for the scoring rules.

The session has nobody to answer, so any prompt blocks indefinitely. Set [security] allow_runtime_expansion = false in the project’s seal.toml so unmatched calls fail immediately instead of prompting — failures show up as exit codes; prompts just stall.

The seal daemon (seal-runtime) didn’t start or crashed. The CLI tries to spawn one automatically; if that fails, check:

  • ps aux | grep seal-runtime — is there a stale daemon from a previous crashed session?
  • ~/.seal/debug.log — the daemon’s last error before exit lands here.

If a stale process is holding the socket, end it cleanly (kill <pid>) — don’t kill -9 unless it’s actually frozen.

Set LC_ALL=en_US.UTF-8 (or your locale’s UTF-8 equivalent) in your shell. The TUI uses unicode box-drawing characters everywhere; non-UTF-8 locales render them as garbage.

Every sandbox-enforced denial lands in ~/.seal/audit/<session-id>.jsonl — one JSON object per line, including the command pattern, the denied path or host, and the timestamp. Useful for after-the-fact “what did the agent try?” questions and for tuning the manifest based on observed traffic.