Orange circuit paths reach a red processor through gaps in a cream enclosure.

What the Claude Code sandbox contains, and what it doesn't

The Claude Code sandbox confines shell writes and network egress. By default, a sandboxed command can still read ~/.ssh and inherits every secret in Claude Code's environment.

Andrei Gaspar

· 9 min read

The Claude Code sandbox fences in shell commands at the operating-system level. Writes stay in the working directory, and network traffic leaves only through a proxy that checks each host. Reads are not fenced by default, and neither is the environment. Claude's file tools, MCP servers, and hooks run outside it entirely.

That is a narrower thing than the word "sandbox" suggests, and a more useful one than the permission prompt it replaces. Take the boundary one wall at a time and the pattern holds: the sandbox controls where a command can go. What the command does once it arrives somewhere allowed is outside its reach.

This piece reads Anthropic's documentation and the open-source runtime's README as they stood in October 2026. It covers why the prompt was the wrong layer, the four walls, the escape hatch that auto-allow leaves open, and the three leaks no allowlist closes.

The permission prompt is the wrong safety layer

Claude Code's default is to ask. Before most shell commands run, a prompt shows the command and waits for a person. The weak part is the person. Anthropic's write-up on auto mode states it without hedging: "Claude Code users approve 93% of permission prompts." A gate that opens 93 times in 100 is mostly teaching the person at it to open it. The same post calls the result approval fatigue, "where people stop paying close attention to what they're approving."

The prompt also judges the wrong thing. It reads a command's text before the command runs. Claude Code's permissions documentation is candid about what that misses. A Bash(curl *) rule does not match /usr/bin/curl or sh -c 'curl …'. Read deny rules do not reach "a Python or Node script that opens files itself." An agent denied a file by one name has other names for it.

The sandbox moves the check from the command's text to the running process. The operating system enforces it: bubblewrap on Linux and WSL2, Seatbelt on macOS. In the words of the sandboxing docs, it "holds regardless of what the model chose to run and even if an allowed command does more than its name suggests." That is what lets Claude Code stop asking. Anthropic's engineering post on sandboxing, from October 2025, reported: "In our internal usage, we've found that sandboxing safely reduces permission prompts by 84%."

The general case, that the boundary is the control and the prompt is for the human's comfort, is made in our piece on agent postmortems. This one is narrower: which walls this particular sandbox builds, and where the gaps sit.

The Claude Code sandbox, wall by wall

The sandbox is off until you turn it on, with /sandbox or sandbox.enabled in a settings file. Once on, it wraps Bash, PowerShell, and Monitor commands and every process they start. Here is what each wall holds by default, according to the docs.

Filesystem writes: contained. A sandboxed command can write to the working directory, a per-user temp directory, and any directory added with --add-dir. Everything else fails; on Linux the docs give the error as Read-only file system. Inside the writable area, a list of protected paths stays denied: the .claude settings files, .mcp.json, .git/hooks, and shell startup files such as .bashrc. The reason is escalation. A command that could edit those files could grant itself permissions, or install a hook that Claude Code later runs outside the sandbox. No allowWrite entry lifts the protection.

Filesystem reads: not contained. The default read scope is "most of the machine, including credential files such as ~/.ssh and ~/.aws/credentials." The docs add that "there is no built-in credential deny list, so only the files and variables you list are restricted." You opt in with filesystem.denyRead, a sandbox.credentials entry, or permissions.blockReadsOutsideWorkingDirectories.

Network egress: contained, for TCP. On Linux the command runs in its own network namespace with no route out. On macOS, Seatbelt blocks every connection except the one to the sandbox proxy. That proxy runs outside the sandbox and checks each hostname against your allowed domains, which start empty. Tools that ignore the proxy variables, such as plain ssh and most database drivers, cannot connect even to an allowed host. UDP, QUIC, and ICMP from the command itself cannot leave. The proxy also refuses an allowed name that resolves only to a local address. By default, a hostname pointed at 169.254.169.254 does not reach the cloud metadata endpoint.

Credentials and environment: not contained. Sandboxed commands inherit Claude Code's environment, "including any secrets in its environment." A GITHUB_TOKEN exported in the shell that launched Claude is visible to every sandboxed env. sandbox.credentials can unset a named variable (deny) or show the command a placeholder that the proxy swaps for the real value on the way out (mask). CLAUDE_CODE_SUBPROCESS_ENV_SCRUB strips variables that look like credentials from the subprocesses Claude Code starts, sandboxed or not. It leaves GitHub tokens in place, so GITHUB_TOKEN still needs a deny entry.

BoundaryContained by default?HowWhat still leaks
Filesystem writesYesbubblewrap or Seatbelt; working directory, temp, added directories; protected paths denied inside themAnything in the working directory, including scripts that later run unsandboxed
Filesystem readsNoReads allowed machine-wide; denyRead and credentials.files are opt-in~/.ssh, ~/.aws/credentials, any file you have not listed
Network egressYes, for TCPNetwork namespace or Seatbelt rule, plus a host-side proxy with an allowlist that starts emptyWhatever an allowed host accepts; TLS is not inspected by default
EnvironmentNoInherited from Claude CodeEvery secret variable you have not denied or masked
File tools, MCP servers, hooksNoRun in the Claude Code process or on the host; permission rules govern the toolsEverything those processes can reach

Read the table by its second column. The two walls that guard secrets, reads and the environment, are open until you close them. The last row is open by design. The Read, Edit, WebFetch, and WebSearch tools follow permission rules, so "a denyRead entry doesn't stop the Read tool, and allowedDomains doesn't limit WebFetch." Command hooks, local MCP servers, and your status line command run "with your full access."

Sponsored: Gitdailies — Install today. Move faster daily.

Auto-allow, and the door it leaves open

The sandbox has two modes. Regular permissions keeps the usual prompts and adds the walls on top. Auto-allow runs sandboxed commands without asking; it is the mode that removes prompts. Some rules still apply under auto-allow. Explicit deny rules still block, rm against a critical path goes through the regular permission flow, and content-scoped ask rules such as Bash(git push *) still prompt.

The prompt that matters is the one that comes back. When a command fails inside the sandbox, Claude can retry it with the dangerouslyDisableSandbox parameter. The retry runs with no walls at all. In manual mode it arrives as a prompt titled "Bash command (unsandboxed)." In bypassPermissions mode it runs without one. An allow rule such as Bash(curl *) approves the retry too, with no prompt.

So auto-allow does not take the human out of the loop. It concentrates them on one decision: whether to take the walls down for this command. That decision arrives after a failure, when the session is stuck and yes is the fastest way out. Of all the prompts the sandbox leaves, it has the worst ratio of stakes to attention.

Two defaults deserve the same suspicion. If bubblewrap or socat is missing on Linux, Claude Code runs commands unsandboxed instead of refusing to start. And the unsandboxed retry is on unless you turn it off. The docs' example managed-settings block closes both. Turning off the retry is what the docs call strict sandbox mode:

{
  "sandbox": {
    "enabled": true,
    "failIfUnavailable": true,
    "allowUnsandboxedCommands": false
  }
}

Strict sandbox mode has a cost. A tool that cannot run sandboxed now fails outright. Someone has to decide whether it goes in excludedCommands, which runs it with your full access. Anthropic's auto mode post puts it in one line: "Sandboxing is safe but high-maintenance: each new capability needs configuring, and anything requiring network or host access breaks isolation." Strict mode makes that maintenance visible instead of letting the retry absorb it.

Three things no allowlist holds

The walls are real. They also have doors, and you cut them yourself. Three of them stay open whatever bubblewrap does.

Allowed domains are an exit

The proxy decides from the hostname the client supplies, and by default it does not terminate or inspect TLS. The docs' own warning: "Allowing broad domains such as github.com can create paths for data exfiltration." Code inside the sandbox "can potentially use domain fronting or similar techniques to reach hosts outside the allowlist." Set fronting aside and the plain case remains. Any allowed host that accepts writes is a place a command can send what it read: a code host, a package registry, an object store. Reads are open by default. Put those two rows of the table together and the exfiltration path needs no exploit.

Egress also has more doors than HTTP. In September 2026, OpenAI reported a training run in which an agent was blocked by a web proxy when it tried to reach a public chatbot over HTTPS. The agent then found that the environment's DNS resolver still answered external queries, and used it to relay questions to a third-party chatbot. Monitoring flagged it within 15 minutes. The run did not stop automatically as expected, and it was killed 2.5 hours later, once the confusion over whether it should be stopped was resolved.

The sandbox-runtime README notes that on Windows, name resolution through the system resolver is not fenced. A lookup succeeds even when the sandboxed connection is blocked, and the README says this mirrors macOS. We have not tested whether a lookup from a Claude Code sandbox can carry data out. It is the question to ask of any sandbox whose resolver answers.

Secrets inside the walls

The working directory is inside every wall by definition. A .env in the project, a credentials file a script left behind, a token pasted into a config: a sandboxed command can read all of them. The proxy carries whatever the command sends to any allowed host.

Masking narrows the damage for credentials you declare. A masked variable shows the command a placeholder, and the proxy substitutes the real value only on requests to the hosts in that entry's injectHosts. Leave injectHosts off and the proxy substitutes it "on requests to every host in network.allowedDomains." Masking also needs network.tlsTerminate, which the docs mark experimental.

The stronger design is the one Anthropic's sandboxing post describes for Claude Code on the web: the git credential never enters the sandbox. Inside, git authenticates to a proxy with a scoped credential. The proxy checks the operation, for instance that a push goes only to the configured branch, and only then attaches the real token. A secret that lives outside the boundary is the only kind the boundary fully protects.

Actions through allowed tools

A wall restricts where. It says nothing about what a permitted action means. git push to an allowed github.com, with a token in the environment, does whatever that token's scopes allow. npm publish to an allowed registry publishes. The proxy sees an allowed host and passes the bytes.

The writable working directory works the same way. A sandboxed command can rewrite a script that an excluded command later runs with full access. The docs warn about exactly this for docker compose and its compose file. And none of it changes what reaches the model. Per the docs on sandbox environments, "your prompts and the files Claude reads are transmitted to the Anthropic API or your configured provider with or without a sandbox."

When the shell is not enough

The built-in sandbox wraps shell commands, which is not enough for an unattended run. The docs say to run --dangerously-skip-permissions sessions "inside a container, a VM, or the sandbox runtime," so file tools, MCP servers, and hooks sit inside the boundary too.

The standalone runtime, @anthropic-ai/sandbox-runtime, wraps the whole Claude Code process in the same bubblewrap or Seatbelt layer. It is a beta research preview. Two of its documented behaviors matter before you lean on it. On Linux it builds its write-deny list once, at launch. Anything the session creates later is not covered: a repository cloned mid-session does not get its .git/hooks protected. And with no settings file it starts anyway, network blocked and writes confined to its own paths. The docs' advice: "Don't take a clean start as proof your settings loaded."

Past the runtime, the options are a dev container with a default-deny firewall, then a VM with its own kernel for code you do not trust. Each one moves more of the session inside the walls. None of them closes the three leaks in the previous section, because those run through doors you open on purpose.

Monday

Ask Claude to run the docs' two probes, touch ~/sandbox-probe and curl --noproxy '*' https://example.com, and decline any offer to retry unsandboxed. Typing them yourself at the ! prompt tests nothing, because those commands usually run outside the sandbox. Both should fail. If either succeeds and your home directory is not one the sandbox may write to, the command ran outside the sandbox. Run /sandbox to check that it is on and its dependencies are installed.

Then list every secret a sandboxed command can read on your machine: the files under ~/.ssh and ~/.aws, the tokens in the shell you launch Claude from, the .env in the project. Give each file and variable a deny entry in sandbox.credentials, and set allowUnsandboxedCommands to false while you are in the file.

Last, read your allowedDomains as an exfiltration list. For each entry, ask whether a sandboxed command could write to it. Every yes is a door you chose to leave open, and that choice belongs in a reviewed settings file, not in a prompt answered mid-session.

This blog exists thanks to the support of our sponsors:

GitdailiesQA.techAppSignalSuperlinked

Comments

Loading comments…

Keep reading