Built-in Bash sandbox: network isolation unreliable on WSL2; failIfUnavailable doesn't detect a non-enforcing proxy
Summary
On WSL2, the built-in Bash sandbox's network isolation is unreliable: the out-of-sandbox proxy (which sandboxed processes reach at localhost:3128) frequently never starts, and when it does start it sometimes does not enforce network.allowedDomains. Filesystem isolation works correctly on the same host. Critically, sandbox.failIfUnavailable: true does not catch this — it verifies the sandbox binaries exist, not that the network proxy actually started and enforces — so the sandbox silently fails open or fails closed with no error.
Environment
- Claude Code 2.1.215
- WSL2, kernel
6.6.87.2-microsoft-standard-WSL2 bubblewrap0.9.0,socatpresent,@anthropic-ai/sandbox-runtime0.0.66 installedsysctl kernel.apparmor_restrict_unprivileged_userns→ key does not exist (no AppArmor userns restriction)- No ambient proxy:
~/.curlrc,/etc/curlrc,git http.proxy, and*_PROXYenv vars all absent (thelocalhost:3128seen inside the sandbox is Claude Code's own injected proxy, not an external one)
Settings (user scope, $HOME/.claude/settings.json)
{
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"allowUnsandboxedCommands": false,
"network": { "allowedDomains": ["myorg.crm.dynamics.com"] }
}
}
Repro
Run headless in a trusted workspace (fresh HOME needs $HOME/.claude.json with projects["<cwd>"].hasTrustDialogAccepted: true, else it runs degraded — see note 3):
claude -p --dangerously-skip-permissions --allowedTools Bash --model sonnet \
'run: curl -sS -o /dev/null -w "ORG=%{http_code}\n" https://myorg.crm.dynamics.com ;
then: curl -sS -o /dev/null -w "EX=%{http_code}\n" https://example.com && echo WEB_OK || echo WEB_BLOCKED'
Observed (across ~10 runs, non-nested, plain terminal)
Three outcomes, non-deterministically:
- Proxy dead (most common): both curls fail with
curl: (7) Failed to connect to localhost port 3128. The allowed org host is unreachable too — not just a policy block. - Proxy up but not enforcing:
ORG=302andexample.comalso succeeds (WEB_OK) — the denied host egresses. - Correct (rare):
ORG=302,example.comblocked.
Expected: deterministically outcome 3 — allowed host reachable, all others blocked.
Notes
failIfUnavailableis binaries-only. It never aborted in any of the above, including when the proxy was dead and every request failed. It does not detect a non-functional/non-enforcing network proxy at runtime, so a broken network sandbox silently fails open (outcome 2, exfiltration risk) or fails closed for all traffic (outcome 1).- Filesystem isolation works on the same host/run (writes outside cwd are denied with
Read-only file system), so bubblewrap itself is functioning — the failure is specific to the network proxy / socat relay into the sandbox netns. - Untrusted workspace degrades silently. With an untrusted fresh HOME,
settings.json'spermissions.allowentries are ignored and the sandbox runs in a degraded mode that injectsHTTP_PROXY=localhost:3128but never starts the proxy → every request dies at the dead proxy. A warning is printed about permissions, but nothing indicates the sandbox itself is non-functional. Trusting the workspace is what enabled outcome 3 to occur at all.
Impact
Any automated/headless use of the built-in sandbox for network isolation on WSL2 cannot rely on it: failIfUnavailable gives a false sense of a fail-closed guarantee, while the actual enforcement is flaky. We had to add a runtime preflight probe (drive one sandboxed request, assert an allowed host is reachable and a denied host is blocked) to fail closed, and document "run on native Linux, not WSL2."
Ask
- Make
failIfUnavailable(or a companion setting) verify the network proxy actually started/enforces, not just that binaries exist. - Fix the WSL2 proxy/socat-relay startup reliability, or document WSL2 as unsupported for sandbox network isolation.
- Consider making untrusted-workspace either enforce the sandbox or fail loudly, rather than injecting a dead proxy.