Sandbox policy covers Bash only; Write and WebFetch are not gated by it

Status Open
Reported on v2.1.246
Maintainer reply None cached
Activity 0 comments · opened Aug 26, 2026

Summary

The sandbox governs shell commands. Tools the model calls in-process do not go through it, so the filesystem policy and the network policy both hold for Bash and are silently absent for Write and WebFetch. This was raised in #26616 and in #4320 before it; both were closed by the inactivity bot rather than by a decision, and the behaviour is still present.

This is not a report of a new bypass. The network half is documented in the CLI's own help text. The point of this issue is that the documentation says it in a place nobody reads while setting the policy, and the filesystem half is not documented at all.

The CLI already says so, for one of the two

sandbox.network.allowedDomains, from the binary's own description:

Enforced for sandboxed commands only — in-process tools such as WebFetch are not gated by this setting.

Measured, network

Sandbox on, deniedDomains: ["example.com"], strictAllowlist: true, one turn asked to reach the same host two ways:

| tool | outcome |
|---|---|
| Bash, curl https://example.com | blocked, http_code 000, exit 56 |
| WebFetch on https://example.com | returned the page |

With allowedDomains: ["api.anthropic.com"] and strictAllowlist: true, WebFetch was pointed at a plain HTTP server on the loopback and the server's own log is the oracle:

code 400, message Bad request version ('\x9f')
"\x16\x03\x01\x00\xe1\x01\x00\x00\xdd\x03\x03..." 400 -

\x16\x03\x01 is a TLS ClientHello. The socket to a host that is not on the allowlist opened. The fetch then failed because the tool upgraded the URL to https against a plain-HTTP server, which is incidental; the allowlist did not stop the connection.

Measured, filesystem

Same asymmetry, other dimension. A profile naming eight denied regions and one allowed one, an agent told to use Write three times and no Bash, then the disk read directly:

| target | on the profile | landed |
|---|---|---|
| <workspace>/control.txt | allowWrite | yes, and it is the control |
| a path under denyWrite | denied | yes |
| a path outside the workspace, on neither list | neither | yes |

So denyWrite does not stop the Write tool and allowWrite does not bound it.

Why this matters more than a normal doc gap

The two settings read as a security boundary. strictAllowlist in particular reads as strict. An operator who sets a deny list and watches Bash get blocked has every reason to believe the policy applies to the session, and the tool the model actually reaches for most is the one that is not covered. It fails in the permissive direction, silently, with no log line saying a tool skipped the policy.

What would close it

Any one of these, in decreasing order of usefulness:

  1. Route in-process tool egress and in-process file writes through the same policy the sandbox enforces for commands.
  2. Failing that, emit a visible warning at session start when a sandbox policy is set while in-process tools are not covered by it, so the gap is loud rather than silent.
  3. At minimum, state the limitation in the settings documentation for both sandbox.network and sandbox.filesystem, not only in one field's description, and say explicitly that Write is not bounded by allowWrite and not stopped by denyWrite.

Environment

macOS 15 (Darwin 25.5.0), Apple silicon.

The quoted description above is present in 2.1.246, checked today. allowWrite, denyWrite, strictAllowlist and allowUnsandboxedCommands are all still present in that build.

The two measurement tables were taken on 2.1.233. I can re-run either on 2.1.246 on request.

Related

  • #26616, same request, closed by the inactivity bot 2026-04-16
  • #4320, predecessor, closed the same way in 2025
  • #89165 and #84863, the two live reports of the same underlying split

Where these came from

Measured while building Januas, a vendor-neutral supervision and containment layer for coding agents. It is not released and there is nothing being sold here; the reason for the issue is that the same split had to be worked around, and the workaround should not be necessary.

View original on GitHub ↗