Claude Code sandbox on Linux: what srt walls off, and what bypassPermissions opens
The built-in sandbox is a real OS boundary around shell commands, and only around them. How strong it is depends on two things the setup screen never shows: your permission mode, and what existed when the command started. A test on Ubuntu of what srt blocks and leaves open, the Linux bug that still breaks it, and when a container or VM fits better.
Contents
Anthropic’s sandbox-runtime is not new. The repository was created on 20 October 2025, and version 0.0.81 shipped on 20 October 2026. It is the engine under Claude Code’s built-in sandbox, and the two get talked about as one thing with one boundary. They are not, and the boundary is less fixed than the word suggests.
I ran both on an Ubuntu 24.04 machine: srt 0.0.81 by hand, and Claude Code 2.1.310 headless with the sandbox switched on. Two results from those runs shape this piece. In bypassPermissions mode, the network half of the sandbox let an unlisted host straight through until I set one more key. And on Linux, the list of protected paths is taken when a command starts, so a repository created inside that command is unprotected until it ends. Both say the same thing: the sandbox you get depends on conditions the /sandbox panel does not display, so the only check I trust is a probe run under your real mode.
What does the Claude Code sandbox actually wall off?
Shell commands and the processes they start, on two sides: files and network. Per the sandbox docs, a sandboxed command can write to the working directory, a per-user temp directory and directories you added, and nowhere else. It has no direct route to the network; connections go through a proxy on your machine that checks each hostname against an allowlist that starts empty. The sandbox is off by default. /sandbox turns it on for a project, sandbox.enabled in ~/.claude/settings.json for all of them.
What it leaves open by default matters as much. Reads are allowed across most of the machine, ~/.ssh and ~/.aws/credentials included. Environment variables are inherited from Claude Code, secrets and all. Both can be narrowed with filesystem.denyRead and the credentials block, and only what you list is covered: the docs say there is no built-in credential deny list.
Then there is everything the sandbox never sees. Read, Edit, Write, WebFetch and WebSearch follow permission rules instead, so a denyRead entry does not stop the Read tool and allowedDomains does not limit WebFetch. Hooks, local MCP servers, LSP servers, plugin monitors, the status line command and apiKeyHelper run with your full access. So does anything a mod starts.
How is Claude Code’s sandbox different from sandbox-runtime (srt)?
Same engine, different scope. Claude Code builds its sandbox on the @anthropic-ai/ package and wraps each Bash, PowerShell and Monitor command in a fresh one. The package also ships a CLI, srt, that wraps any process: one command, a local MCP server, or Claude Code itself, in which case file tools, hooks and MCP servers end up inside the boundary too. The README still calls it a beta research preview whose configuration format may change.
On Linux the boundary is bubblewrap. The sandboxed process gets read-only binds over everything it may not write, and a network namespace with no route out. Two proxies run on the host, one for HTTP and one SOCKS5, and reach the sandbox over Unix sockets that socat bridges in. A bundled seccomp helper blocks creating new Unix sockets, and blocks the system calls that would let a command create a user namespace of its own, since inside one it could unmount the read-only binds. On macOS the same policy is a generated Seatbelt profile. On native Windows, srt has an alpha that runs commands under a dedicated local account; Claude Code itself runs commands unsandboxed there unless you move it into WSL2.
What does bypassPermissions do to the Claude Code sandbox network wall?
It answers the network question for you, and the answer is yes. When a sandboxed command connects to a host outside the allowlist, Claude Code lets your permission mode decide. Manual and acceptEdits prompt you, dontAsk refuses, and auto mode refuses unless Claude listed the host on the command beforehand and the classifier approved the list. In bypassPermissions, per the docs, the connection is allowed without a prompt. I ran the same curl to example.com in four headless sessions with the sandbox on, the unsandboxed retry off and no allowed domains at all:
| Permission mode | strictAllowlist | Result |
|---|---|---|
| default | off | blocked: deny network-outbound example.com:443 (user denied) |
| default | on | blocked: host is not on the allow list |
bypassPermissions | off | 200 |
bypassPermissions | on | blocked: host is not on the allow list |
The first row has a caveat. A headless claude -p session has nobody to answer a prompt, so “user denied” is the prompt failing closed; in an interactive session you would have been asked. I did not run auto mode.
The third row is the one I came for. In the same mode, touch ~/sandbox-probe still failed with Read-only file system, so with the retry off the filesystem half held while the network half let an unlisted host through. With the retry left at its default, the docs add that a failed command’s unsandboxed retry also runs in bypassPermissions without a prompt, and then neither half holds for that command. strictAllowlist (v2.1.219 and later) is the key that closes the network side: it refuses unlisted hosts in every mode, and it has no effect when set in a repository’s .claude/settings.json. Two more keys harden the rest:
{
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"allowUnsandboxedCommands": false,
"network": { "strictAllowlist": true, "allowedDomains": ["github.com", "registry.npmjs.org"] }
}
}allowUnsandboxedCommands: false makes Claude Code ignore the retry. failIfUnavailable stops it at startup when the sandbox cannot run, instead of the default, which is to run commands without one. Where you put this matters. Passed with --settings or delivered as managed settings, turning the retry off makes the sandbox admin-required, and Claude Code then ignores a repository’s excludedCommands and other loosening entries. In plain user settings it does not, and a repository’s excludedCommands can still take matching commands out of the sandbox, so check what is excluded either way.
Even locked down, the allowlist works on hostnames and does not inspect TLS. The docs warn that a broad entry such as github.com can be an exfiltration path and that domain fronting may reach hosts outside the list. Every domain you add is a channel, which is the inventory problem from every way out of the sandbox, shrunk to one machine.
How do you run sandbox-runtime (srt) on Linux?
You need bubblewrap, socat and ripgrep, Node for the package, and a settings file. Check the kernel side first:
bwrap --version
sysctl kernel.apparmor_restrict_unprivileged_usernsIf the sysctl prints 1, the default AppArmor policy on Ubuntu 24.04 and later, bubblewrap gets user namespaces without the capabilities it needs. The sandbox docs give an AppArmor profile for /usr/bin/bwrap; the README gives the blunter fix of setting the sysctl to 0. On the test machine it already read 0.
With root, the dependencies are one line each:
sudo apt-get install bubblewrap socat ripgrep
npm install -g @anthropic-ai/sandbox-runtimeThe test machine had bubblewrap but neither socat nor ripgrep, which the standalone runtime looks for on PATH. Without root you can unpack the Debian packages into a directory you own. Keep that directory and the package outside every path you will allow writes to. The README warns that a copy in a writable node_modules can be rewritten from inside the sandbox and then run unsandboxed next time, and the same goes for a writable rg or socat on the path.
mkdir -p ~/opt/srt-deps && cd ~/opt/srt-deps
apt-get download socat ripgrep
for f in *.deb; do dpkg-deb -x "$f" root; done
npm install --prefix ~/opt/srt @anthropic-ai/sandbox-runtime@0.0.81
export PATH="$HOME/opt/srt-deps/root/usr/bin:$HOME/opt/srt/node_modules/.bin:$PATH"Then work from a separate scratch directory, which is what "allowWrite": ["."] will open, and keep the settings there or in ~/.srt-settings.json. This is the file I used for the network and credential tests:
mkdir -p ~/srt-lab && cd ~/srt-lab && git init -q
cat > srt-test.json <<'EOF'
{
"network": { "allowedDomains": ["example.com"], "deniedDomains": [] },
"filesystem": { "denyRead": ["~/.ssh"], "allowWrite": ["."], "denyWrite": [] },
"credentials": { "envVars": [{ "name": "DEMO_TOKEN", "mode": "deny" }] }
}
EOFOne trap from the README’s own examples. srt "curl example.com" with the command in quotes failed for me with command not found, because 0.0.81 took the whole string as the program name. A command string goes after -c:
srt --settings ./srt-test.json -c 'curl -sS -o /dev/null -w "%{http_code}\n" https://example.com'What did sandbox-runtime block on Ubuntu, and what did it let through?
Every write and connection I aimed outside the policy failed, and the defaults let through exactly what the docs say they let through. Each line is one srt -c run from the scratch directory:
| Probe | Result under srt 0.0.81 |
|---|---|
touch ~/sandbox-probe | Read-only file system |
| write a file in the working directory | allowed |
append to .bashrc in the working directory | Permission denied |
write .git/hooks/pre-commit | Read-only file system |
write to /tmp | Read-only file system |
write to $TMPDIR (/tmp/claude) | No such file or directory until the directory existed on the host, then allowed |
list ~/.ssh with no denyRead | allowed, 15 entries |
list ~/.ssh with denyRead | allowed, 0 entries |
curl a host with an empty allowlist | CONNECT tunnel failed, response 403 |
curl an allowed host | 200 |
curl --noproxy '*' to an allowed host | Could not resolve host |
send a UDP packet to 1.1.1.1:53 | Network is unreachable |
curl a server on the host’s 127.0.0.1 | Couldn't connect to server |
start a server inside the sandbox and curl it | 200 |
| connect to the D-Bus system socket | Operation not permitted |
unshare -U true | Operation not permitted |
DEMO_TOKEN (listed) and OTHER_TOKEN (not listed) | DEMO_TOKEN unset, OTHER_TOKEN visible |
A read-denied directory does not fail to open, it appears empty: bubblewrap mounts an empty tmpfs over it, so a tool looking for a key file finds none rather than an error. The token row is the credentials rule in miniature: whatever you forget to list is not withheld. The temp-directory row matches a line in the environments page: on Linux, write grants apply only to paths that already exist.
The localhost pair is the Linux behavior the docs describe. The sandbox has its own loopback, so a direct connection to a dev server or a database on the host fails, and allowLocalBinding does nothing on Linux. Through the proxy, a host service is reachable only if you put it on the allowlist, as localhost or as an address and port such as 127.0.0.1:3000; any other hostname that resolves to a loopback address is refused. Inside the sandbox, env also showed the plumbing: HTTP_PROXY, ALL_PROXY and their relatives, a GIT_SSH_COMMAND that tunnels git’s SSH through the proxy, and a JAVA_TOOL_OPTIONS agent for JVM tools that ignore proxy variables.
Where does the sandbox-runtime deny list miss on Linux?
On protected paths that appear inside a running sandbox. The environments page says it directly: on Linux the runtime builds its deny list once at launch, covers the project root, scans shallowly for nested copies that already exist, and does not cover what the session creates later. One run shows it:
srt --settings ./srt-test.json -c 'git init -q sub && echo "echo hi" > sub/.git/hooks/pre-commit && echo wrote-hook'
# wrote-hook
srt --settings ./srt-test.json -c 'echo "echo hi" > sub/.git/hooks/post-commit'
# /usr/bin/bash: line 1: sub/.git/hooks/post-commit: Read-only file systemThe repository created inside the first command took a hook. The next srt run built a fresh deny list, found the repository and protected it. A .bashrc in a subdirectory made inside one command went through the same way. On macOS the denies are checked at each write, so this gap is Linux-only. How big it is depends on what the sandbox wraps. Claude Code’s built-in sandbox starts a new one per command, so the window is one command long. srt claude wraps the whole session in one sandbox, so a repository the session clones or scaffolds stays outside the list until you restart.
The same mechanism leaves something behind. To protect a file that does not exist yet, the Linux sandbox puts a placeholder at its path while the command runs. In an empty directory, a four-second sleep under srt created twelve of them, from .bash_profile and .bashrc to .mcp.json, .vscode and .idea, visible from the host the whole time. A clean exit removed them. After a kill -9 they stayed: empty, read-only, owned by me. The sandbox docs describe the same leftovers at .claude settings paths and point to claude doctor to find them. In a project directory that is clutter. With your home directory as the working directory, an empty ~/.bash_profile is a real change, because a login bash reads it and then skips ~/.profile.
Why does the Claude Code sandbox fail on some Linux machines?
One regression is still open. Issue 43454: since Claude Code 2.1.92 shipped the apply-seccomp helper, every sandboxed Bash command can fail during setup on systems where user namespaces come without capabilities, which includes the default AppArmor policy on newer Ubuntu. The original report:
apply-seccomp: write /proc/self/setgroups (nested userns is capability-restricted; caller must provide CAP_SYS_ADMIN): Permission denied
It is labeled regression and reproduced. A maintainer’s comment from August confirms it on 2.1.233 and lists workarounds: set kernel., which weakens system hardening, and note that the documented AppArmor profile covers bubblewrap only, so it may not be enough on its own. Put together with the setup section, that is the trap: on a stock Ubuntu 24.04 or later, the documented fix gets bubblewrap working and can still leave the helper failing. One commenter’s root-cause analysis puts the failure on the helper writing setgroups inside a namespace where bubblewrap has already made that write impossible, and reports it failing with the sysctl at 0. The issue was still open on 24 October 2026. It did not reproduce on my test machine, and I can’t say why from one box, so a clean run here says little about yours.
The other complaints in that area are mostly documented behavior. A command with a variable in front, FOO=bar npm test, is not auto-approved in auto-allow mode; issue 50509 reported it as a bug, and the docs now explain it as intended and give the fix, an allow rule that includes the assignment. docker does not work inside the sandbox. Plain ssh and most database drivers ignore the proxy variables and cannot connect even to an allowed host, while git over SSH works on Linux once the host is allowed. Chromium’s own sandbox and rootless Podman fail because they need user namespaces. Each fix in the troubleshooting section moves a command out of the sandbox or lifts a restriction, and says which.
When is a VM or a container better than the Claude Code sandbox?
When the thing to contain is more than shell commands, which covers any unattended run. The environments page says the Bash sandbox alone is not sufficient for fully unattended work, and that --dangerously-skip-permissions sessions should always run inside a container, a VM or the sandbox runtime. It also calls the auto mode classifier a per-action control rather than an isolation boundary, which is the argument of the approval gate is a classifier: put the unattended limit on the reach the harness grants.
The options trade scope against setup. The built-in sandbox is for attended work with fewer prompts. srt claude puts the whole process inside one bubblewrap boundary without Docker; per the docs it needs write access to the project, ~/.claude, ~/.claude.json and /tmp, network access to api.anthropic.com and, for OAuth, claude.ai and platform.claude.com, plus a denyWrite over the configuration paths those grants expose, or a session can leave hooks and MCP servers that run unsandboxed next time. A dev container, such as the reference one with its default-deny firewall, does the same with Docker, and how strong it is depends on what you mount, which credentials sit inside it and what the firewall allows. A VM adds its own kernel, and the docs recommend one, or a cloud session, for untrusted repositories.
My read on where the line falls: the built-in sandbox is a good wall around commands someone is watching. Once nobody is, the boundary should hold the whole process and its network, and the check that matters most is the one the setup screen never shows, what your permission mode does with a host that is not on the list. Ask Claude to run the probes under the mode you use day to day, with the retry turned off, and decline any offer to rerun them unsandboxed: touch ~/sandbox-probe for the write wall, curl --noproxy '*' to any site for the direct route, and a plain curl to a host you never allowed for the allowlist. Believe their output over the panel.
01 / Discussion
No comment section here — all discussions happen on X.
02 / More articles
Claude auto mode becomes the default in the Claude apps: what the check blocks and what 89% measured
Anthropic's study is honest about its setup: tired testers against one planted command. It reports no false-positive rate and no miss rate on real traffic, and in cloud sessions there is no Manual mode to switch back to.
Claude Code cost in 2026: what Pro, Max and the API charge for the same work
The plan prices are flat and the token prices are public, so the comparison looks like arithmetic. It isn't quite, because Claude Code caches your conversation differently depending on how you pay, and the tool most people use to put a dollar figure on subscription usage prices the cache you had, not the one you would get.
Claude Code hooks: good at steering, weak as a gate
A hook runs whether the model agrees or not, which makes it good at chores like formatting and at steering an agent toward the route you want. As a gate it is weaker than it looks: it checks only the tools you point it at, a broken check lets the call through unless you opt into blocking, and in some setups a mod answers after it.