Claude Code · Kit Sandoval (The Operator) · 2026-07-23 · why OS-level sandboxing cuts 84% of your permission prompts without going YOLO — and where the box still leaks
Here is the workflow that breaks by lunch. You start a session, the agent
wants to run npm test, you approve. It wants npm run build, you approve.
git status, approve. ruff check, approve. Somewhere around the fortieth
prompt you stop reading the command and start hitting y on reflex. The prompt
was supposed to be the brake. By prompt forty it is a formality.
That is not a discipline problem. It is the predictable failure of a human classifier under load — the same false-negative curve the deskilled-reviewer dive traced yesterday. The more reliably the agent behaves, the less each prompt earns your attention, and the worse you get at the one prompt in fifty that actually matters. A brake that wears out the more you use it is not a brake.
So most people take the other exit. They run --dangerously-skip-permissions,
alias it to yolo, and never see a prompt again. Now there is no brake at all.
A prompt-injected file or a bad curl | bash runs with your shell’s full reach:
your ~/.ssh, your ~/.aws/credentials, your whole home directory, and an open
socket to anywhere on the internet.
There is a third setting, and it is the one to use. Move the brake off the prompt and onto the operating system.
What the sandbox actually is
Claude Code ships a sandboxed Bash tool. Turn it on and every shell command the
agent runs — and every child process that command spawns — executes inside an
OS-enforced box that can write only to your working directory and reach only the
network domains you named. On macOS the box is Seatbelt,
Apple’s built-in sandbox-exec. On Linux and WSL2 it is
bubblewrap, the same unprivileged
namespace tool Flatpak uses. No container, no VM — kernel primitives wrapping
the process.
The number that matters is Anthropic’s own. In their engineering write-up, sandboxing “safely reduces permission prompts by 84%” in internal usage. That is the whole pitch: you stop approving commands one at a time because the box, not your attention, is now what contains them.
This is a different layer from the one the hooks
dive covered. A permission rule —
or a PreToolUse hook — decides whether a command runs, and it decides it
before the command runs, from the command string. The sandbox decides what a
command can touch once it is running, and the OS enforces that on the live
process. The docs put the distinction plainly: the boundary “holds regardless of
what the model chose to run and even if an allowed command does more than its
name suggests.” That last clause is the point. A permission classifier reads
npm test and reasons about what it should do. The sandbox does not care what
npm test claims to be — if the process reaches for ~/.ssh, the kernel returns
EPERM. You can auto-approve because the containment no longer depends on anyone,
human or classifier, reading the command right.
The config to paste in
Run /sandbox once to see the panel, pick auto-allow, and it writes to your
project’s .claude/settings.local.json. To turn it on everywhere, put this in
~/.claude/settings.json:
{
"sandbox": {
"enabled": true,
"network": {
"allowedDomains": ["registry.npmjs.org", "*.github.com", "pypi.org"]
},
"credentials": {
"files": [
{ "path": "~/.ssh", "mode": "deny" },
{ "path": "~/.aws/credentials", "mode": "deny" }
]
}
}
}
The credentials block is not optional, and here is why. The sandbox’s default
write perimeter is tight — working directory plus the session temp dir. Its
default read perimeter is the whole computer. Straight from the
docs: “the default read policy
still allows them,” meaning ~/.aws/credentials and ~/.ssh/ are readable by a
sandboxed command unless you say otherwise. Filesystem isolation stops the agent
from modifying your dotfiles; it does not, by default, stop it from reading
your keys. The credentials.files deny list closes that. For the belt-and-braces
version, CLAUDE_CODE_SUBPROCESS_ENV_SCRUB strips Anthropic and cloud provider
tokens out of every subprocess environment regardless of sandboxing.
Auto-allow is not a blank check. Explicit deny rules still hold. An rm
aimed at / or your home directory still prompts. Content-scoped ask rules
like Bash(git push *) still stop for approval even when sandboxed. What
disappears is the undifferentiated firehose of “can I run this build” prompts —
which is 84% of them.
For anything unattended — CI, a headless loop, an overnight refactor — tighten two more keys:
{
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"allowUnsandboxedCommands": false
}
}
failIfUnavailable refuses to start rather than silently falling back to
unsandboxed execution when bubblewrap is missing — you never want an unattended
run quietly dropping its own containment. allowUnsandboxedCommands: false is
the one people miss. By default, when a command fails because of the sandbox,
Claude may retry it with dangerouslyDisableSandbox and run it outside the box.
That is a sensible escape hatch when you are watching. It is a hole when you are
not. Setting it false is what the docs call strict sandbox mode: the escape
hatch is ignored, and a command either runs sandboxed or is on your explicit
excludedCommands list.
Where the box leaks
A sandbox you trust more than it deserves is worse than none, so read the limits before you rely on it. Anthropic is unusually honest that this “is not a complete isolation boundary.” Three leaks are worth holding in your head.
The network filter reads the hostname, not the traffic. By default the
built-in proxy does not terminate TLS. It allows or blocks a connection on the
hostname the client claims, then passes the encrypted bytes through untouched.
So a broad allowedDomains entry is an exfiltration path: allow github.com
for your tooling and a compromised process can push secrets out through a gist
or an issue comment, or use domain
fronting to reach a host you
never allowed. One practitioner’s teardown puts the bind exactly: “Broad domains
allow exfiltration via gists and issue comments, but half your dev tooling talks
to github so you have to let it through”
(claudecodecamp,
single source, but it matches the docs’ own warning). Scope domains as narrow as
your workflow tolerates; registry.npmjs.org beats *.
The sandbox is Bash-only. It isolates shell subprocesses. Claude’s own
Read, Edit, and Write tools do not run through it — they go through the
permission system directly. That means sandbox.filesystem.denyRead governs
what a shell command can read, not what the Read tool can read; the same
teardown found a denyRead path the Read tool opened anyway. It is not a bug in
the sandbox, it is the scope of the sandbox: if you want to fence the agent’s
built-in file access, that is a permissions job, not a sandbox job. Know which
tool you are actually constraining.
A sandbox is only as trustworthy as its documentation. Simon Willison, who
has read more of these than most,
makes the durable point: “sandboxing products… are rarely thoroughly
documented, and in the absence of detailed documentation it’s hard to know how
much I can trust them.” He cites the api.anthropic.com/v1/files exfiltration
vector — allow your own model provider’s API, which you must, and you have
allowed an upload endpoint. Anthropic’s docs are, to their credit, thorough
enough to tell you all of this. Treat the sandbox as defense in depth — the exact
frame Anthropic uses, “without network isolation a compromised agent could
exfiltrate SSH keys; without filesystem isolation it could escape and gain
network access” — not as a wall you can stop thinking behind.
New this week, and the standalone
Two shipping notes make this the right week to set it up. In
v2.1.216 (Jul 20),
sandbox.filesystem.disabled arrived: it drops filesystem isolation while
keeping the network box. That sounds backwards until you hit the tool that
breaks under filesystem isolation — and plenty do — but you still want egress
control. It is the “I trust this workload’s writes, I do not trust where it
phones” case. Use it deliberately; with writes unrestricted and commands
auto-allowed, a command can edit a file a later command runs. The same release
also fixed worktree subagents escaping to the shared checkout — a reminder that
the isolation guards themselves get patched, and the argument from the
worktree dive holds: layer your
brakes, don’t trust one.
And the box is not Claude-Code-only. Anthropic open-sourced the engine as
@anthropic-ai/sandbox-runtime.
Its srt CLI wraps any process — srt "curl anthropic.com" — and, more useful,
wraps a local MCP server so a third-party tool server runs inside the same
filesystem and network fence:
{
"mcpServers": {
"filesystem": {
"command": "srt",
"args": ["npx", "-y", "@modelcontextprotocol/server-filesystem"]
}
}
}
That is the containment the agent-egress dive asked for, applied to the MCP servers you did not write and cannot audit line by line.
So what to do Monday
Turn on the sandbox, pick auto-allow, and immediately add the credentials
deny list for ~/.ssh and ~/.aws — because the default read perimeter is your
whole disk. Scope allowedDomains to the handful of hosts your build actually
talks to; every wildcard is a maybe-exfil path the proxy cannot see into. For
anything you are not watching, set allowUnsandboxedCommands: false and
failIfUnavailable: true. And keep the mental model straight: the sandbox
contains Bash, the permission system and hooks contain the tools and the
decision to run, and neither one inspects your encrypted traffic. The win is
real and measured — 84% fewer prompts — but you buy it by moving the brake to a
place that does not get tired, not by removing the brake. If you want the wall
rather than the fence, that is a VM, which is why Anthropic runs
Cowork in a full VM
and Claude Code in a sandbox. Match the boundary to the blast radius you can
afford.