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.
Sandbox denials
Section titled “Sandbox denials”sandbox denied read: <path>
Section titled “sandbox denied read: <path>”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:
- 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. - Toggle a relevant bundle. Most tool-config dirs (
~/.cargo,~/.gitconfig, etc.) come for free with the matching[sandbox.os] command_toolsbundle entry —command_tools = ["cargo"]adds the cargo config dirs without you naming each one. - Loosen the baseline.
[sandbox.os] command_fs = "permissive"adds$HOMEread-only to every command’s baseline. Use sparingly — secrets stay masked but every other config dir becomes visible to every command.
sandbox denied write: <path>
Section titled “sandbox denied write: <path>”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.
sandbox denied command: <pattern>
Section titled “sandbox denied command: <pattern>”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.
not in allowlist
Section titled “not in allowlist”The legacy form of sandbox denied command. Same fix.
Network failures
Section titled “Network failures”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 atsandbox.os.command_toolslist 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 = truekeeps 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.
Manifest signature
Section titled “Manifest signature”signature mismatch
Section titled “signature mismatch”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.tomlfrom 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).
Provider auth
Section titled “Provider auth”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.
OPENROUTER_API_KEY not set
Section titled “OPENROUTER_API_KEY not set”You picked provider = "openrouter" but didn’t set the env var. Export OPENROUTER_API_KEY=sk-or-v1-... and re-launch.
Model not found
Section titled “Model not found”The name field references a model the provider doesn’t expose to your account. Common causes:
- Anthropic:
claude-haiku-4-5requires an account tier that has access; default toclaude-sonnet-4-6if you’re not sure. - OpenRouter: vendor prefix is required —
claude-sonnet-4-6won’t work; useanthropic/claude-sonnet-4-6.
Permission prompt loops
Section titled “Permission prompt loops””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.
Prompts in a CI / unattended run
Section titled “Prompts in a CI / unattended run”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.
TUI / startup
Section titled “TUI / startup”couldn't connect to daemon
Section titled “couldn't connect to daemon”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.
TUI renders corrupted glyphs
Section titled “TUI renders corrupted glyphs”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.
Audit log
Section titled “Audit log”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.
Still stuck?
Section titled “Still stuck?”- The full per-section manifest reference covers every knob.
- Bug reports + feature requests: github.com/sealedsecurity/seal/issues.