Skip to content

Guide · Defence in depth

Claude Code sandbox vs guardrails: what each can’t stop

A sandbox limits where a shell command can write and which hosts it can reach. Guardrails decide which actions you allow at all, and keep a record of what ran. Each misses what the other catches — so run both.

by: myrmitis LLC

Updated 2026-09-25

The Claude Code sandbox is an operating-system boundary — Seatbelt on macOS, bubblewrap on Linux and WSL2 — around every Bash command and everything it spawns. It decides where a command may write and which hosts it may reach. It doesn’t know what a command means: an authenticated request that deletes a production volume looks like any other request to an allowed host. Deterministic guardrails read the call itself and deny the shapes you name, but can’t see inside an interpreter. Run the sandbox for the machine, rules for the lines you drew, and a ledger for the record.

Both, in two steps
# inside Claude Code: the OS boundary for Bash
/sandbox

# in your repo: rules, caps, claim checks and a ledger
npx @bulkheadtools/cli init
A$ curl -X POST https://backboard.railway.app/graphql/v2 -H "Authorization: Bearer $RAILWAY_TOKEN" -H "Content-Type: application/json" -d '{"query":"mutation { volumeDelete(volumeId: \"vol_7f3a\") }"}'
1 · Bulkhead reads the callDeniedGraphQL delete mutation sent to a remote API
2 · The sandbox runs itNever runsThe hook denied it first, so nothing ran. Had it run, the proxy would have seen only a hostname.
B$ python3 -c "import shutil; shutil.rmtree('/Users/me/backups')"
1 · Bulkhead reads the callAllowed · known missKnown miss: it reads the command string, not the Python inside it.
2 · The sandbox runs itBlocked/Users/me/backups is outside the working directory, and the interpreter is a child process.
Two rows of Bulkhead’s corpus, each through both layers. Bulkhead’s verdicts are the engine’s; the sandbox’s follow Anthropic’s documentation of the default configuration.

What the Claude Code sandbox contains

From Anthropic’s sandboxing docs, in the default configuration:

  • Scope. Bash, PowerShell and Monitor commands and their child processes. Read, Edit and Write go through permission rules instead; MCP servers and hooks run on the host.
  • Writes. The working directory, directories you’ve added, and the session’s temp directory. Everything else — your home directory, ~/.bashrc, /bin — is off limits, and so are Claude Code’s own settings files inside the project.
  • Reads. The whole machine except a few denied directories, which the docs point out still includes ~/.aws/credentials and ~/.ssh/ until you configure sandbox.credentials or denyRead.
  • Network. Through a proxy that allows hosts by name. No domain is pre-allowed: a new one prompts you, or in auto mode goes to the classifier with the command. By default the proxy doesn’t inspect TLS, so it sees the hostname, not the request.
  • Escape hatch. When a command fails inside the sandbox, Claude may retry it outside with dangerouslyDisableSandbox, which goes through your normal permission flow. "allowUnsandboxedCommands": false removes the option.
“Sandboxing reduces risk but is not a complete isolation boundary.” — Claude Code docs

What deterministic guardrails add

Bulkhead reads every tool call before it runs — Bash and file tools alike — and checks it against bulkhead.yaml. The checks are code, not a model: the same call gets the same answer every time, and the agent can’t argue past it.

  • Commands you can’t undo. The engine denies 26 of the 26 dangerous calls in its default corpus, and 6 more with --preset strict.
  • Protected paths for file tools. .env, prod/**, migrations/** (repo root), and Bulkhead’s own policy, ledger and hook settings.
  • Claims against evidence. An agent that says “all tests pass” when the last test run failed is sent back to work once, with the evidence quoted; a repeat stop is recorded on the receipt, not blocked.
  • Dollar caps and a loop freeze. Per session, per day, and the same call 8 times inside 300 seconds. More in a hard dollar cap per session.
  • A receipt. Every decision lands in a hash-chained ledger in the repo; bulkhead receipt turns a session into a summary for the PR.

What it can’t do mirrors the sandbox. It reads the command string, so the 5 known misses in its corpus — an interpreter, a script written a moment earlier, a cd before a relative rm, an encoded command, a failed mkdir — pass by design. They are published so you know exactly where the line is.

Side by side: the same actions, two boundaries

Default settings on both sides. The Bulkhead column is the engine’s own run over its corpus; the sandbox column follows Anthropic’s documentation.

Claude Code sandbox vs Bulkhead, default configuration
Tool callClaude Code sandboxBulkhead
Recursive delete of a folder under the home directory$ rm -rf ~/backupsBlockedA write outside the working directory.Deniedrm of the home directory
Delete through an interpreter instead of rm$ python3 -c "import shutil; shutil.rmtree('/Users/me/backups')"BlockedChild processes are inside the boundary.Allowed · known missIt reads the command string, not the Python.
Decode an encoded command and pipe it to a shell$ echo cm0gLXJmIH4vYmFja3Vwcw== | base64 -d | shBlockedDecoded, it is rm -rf ~/backups: still a write outside the working directory.Allowed · known missObfuscation defeats pattern matching.
Delete a production volume through a platform's GraphQL API with a found token$ curl -X POST https://backboard.railway.app/graphql/v2 -H "Authorization: Bearer $RAILWAY_TOKEN" -H "Content-Type: application/json" -d '{"query":"mutation { volumeDelete(volumeId: \"vol_7f3a\") }"}'Runs once the host is allowedThe proxy allows hosts by name and doesn’t inspect TLS by default — it never sees the mutation.DeniedGraphQL delete mutation sent to a remote API
Recursively delete a backups bucket$ aws s3 rm s3://acme-prod-backups --recursiveRuns once AWS is allowedThe bucket isn’t on your disk.DeniedRecursive S3 delete
Force-push over main$ git push --force origin mainRuns once github.com is allowedThe branch lives on the remote.Deniedforce-push to a protected branch
Overwrite the .env fileWrite .envOut of scopeFile tools go through permission rules, not the sandbox.Deniedprotected path (.env)
Read cloud credentials$ cat ~/.aws/credentialsAllowed by defaultReads cover the whole machine until you set sandbox.credentials or denyRead.Not guardedProtected paths cover writes and deletes, not reads.
“All tests pass.” — the last test run exited 1Not its jobClaim ≠ evidenceSent back to work with the evidence.
Any tool call once session spend reaches the capNot its jobPaused at capEvery tool call denied until a human raises the cap.

“Blocked” in the sandbox column means blocked inside the sandbox. A retry outside it goes to your permission prompt — or to auto mode’s classifier — unless allowUnsandboxedCommands is false.

The Ona write-up: one layer gets routed around

In March 2026 Leonardo Di Donato of Ona published how Claude Code got around both layers in an Ona environment, an isolated cloud development environment. The permission system denied npx and node; the bubblewrap sandbox ran in auto-allow mode. Told to find a way to run npx anyway, the agent read the deny rule and found that /proc/self/root/usr/bin/npx resolves to the same binary without matching the pattern. When the sandbox stopped that, it moved, unprompted, to disable the sandbox, and Claude Code asked for approval before running unsandboxed.

“Approval fatigue turns a security boundary into a rubber stamp.” — Leonardo Di Donato, Ona

Nothing failed in an exotic way. A pattern over a string matched exactly what it was written to match; a sandbox with an escape hatch deferred to a human. The write-up’s conclusion is the argument of this page: “No single control solves this.” In practice:

  1. Treat any string rule — Claude Code’s deny list or Bulkhead’s corpus — as a floor, not a wall. That is why the known misses are published.
  2. For unattended runs, close the escape hatch with allowUnsandboxedCommands: false.
  3. Put the policy and the ledger out of the shell’s reach too, so neither layer is the only thing holding them.

A combined setup

  1. Sandbox the shell. Run /sandbox, then add this to the project’s .claude/settings.json. Project settings resolve ./ to the repo root. The denyWrite entries back Bulkhead up at the OS layer: Bulkhead denies the obvious shell writes to its policy and ledger (sed -i, truncate, redirects), but not a script or interpreter that does the same. Drop a denyRead line only if a task needs those keys.
.claude/settings.json
{
  "sandbox": {
    "enabled": true,
    "allowUnsandboxedCommands": false,
    "filesystem": {
      "denyWrite": ["./bulkhead.yaml", "./.bulkhead"],
      "denyRead": ["~/.aws", "~/.ssh"]
    }
  }
}
  1. Draw the lines. The strict preset adds rules for commands that are destructive but routine — right for runs nobody is watching. Bulkhead’s hooks run on the host, outside the Bash sandbox, so they can still write the ledger.
Unattended runs
npx @bulkheadtools/cli init --preset strict
  1. Isolate the whole process for unattended work. Anthropic’s sandbox environments guide says to run --dangerously-skip-permissions sessions only inside a container, a VM or the sandbox runtime, so file tools, MCP servers and hooks are inside the boundary too. Bulkhead doesn’t support bypass mode (a public bug report says hooks may not block reliably there), so pair the container with auto mode. Behind a default-deny firewall the hooks’ npx needs the npm registry, or bake the CLI into the image and point the hooks at it with bulkhead init --command.
  2. Keep the receipt. bulkhead receipt writes the session up for the PR; anyone can check the chain it came from.
Check the ledger
npx @bulkheadtools/cli ledger verify

What neither one does

  • Stop prompt injection. A sandbox limits what an injected instruction can reach and rules deny some of what it asks for, but neither can tell a hostile instruction from yours.
  • See data leave through an allowed host. Anthropic warns that broad domains such as github.com can become exfiltration paths.
  • Stay on by themselves. --bare skips hooks; the sandbox is a setting. Whoever runs the session can turn either off.
  • Replace judgment. Auto mode’s classifier is a third layer with different strengths — see auto mode vs deterministic rules, Claude Code hooks for safety, and the incidents these layers are built around.

Claude Code sandbox: questions

Is Bulkhead a sandbox?

No. It reads each tool call before it runs and applies rules you can read in bulkhead.yaml; it doesn't isolate processes, files or the network. Pair it with /sandbox, a container or a VM.

Does /sandbox cover Claude Code's Read, Edit and Write tools?

No. Anthropic's docs say the sandbox isolates Bash, PowerShell and Monitor commands and their child processes; the built-in file tools go through the permission system instead. Bulkhead's protected paths apply to those file tools.

Do Bulkhead's hooks still work with the sandbox on?

Yes. With the sandboxed Bash tool, hooks run on the host rather than inside the sandbox, so they see every tool call and can write the ledger. Inside a container or the sandbox runtime they run inside that boundary, like everything else.

Can the agent turn the sandbox off?

It can ask. When a command fails inside the sandbox, Claude may retry it with the dangerouslyDisableSandbox parameter, which goes through your normal permission flow. Setting allowUnsandboxedCommands to false makes Claude Code ignore that parameter.

Which should I set up first?

For unattended runs, start with whole-process isolation — a container, a VM or the sandbox runtime — then add rules and caps inside it. For everyday interactive work both take a minute: turn on /sandbox inside Claude Code, then run npx @bulkheadtools/cli init.

Let it cook. Keep the receipts.

One command installs the hooks and a policy file you own. Want to watch it refuse something first? The demo runs the incident corpus through the real hook in a throwaway repo.

Rather have a person read your setup? Book a Policy Review ($490). Opens Stripe checkout. After you pay, a person emails you the intake questions.

Your copy
npx @bulkheadtools/cli init

Or watch it refuse first:npx @bulkheadtools/cli demo