Sandbox network allowedDomains not enforced for HTTPS CONNECT tunneling

Status Open
Reported on v2.1.241
Maintainer reply None cached
Activity 3 comments · opened Aug 24, 2026

Bug: Sandbox network allowedHosts not enforced for HTTPS CONNECT tunneling

Summary

The Bash tool's sandbox documents a network allowlist (allowedHosts in .claude/settings.json) that is supposed to restrict outbound requests to a specific set of hosts, with everything else "blocked at the network layer." In practice, requests to hosts not on that list succeed anyway — the sandbox proxy is in the request path (confirmed via HTTPS_PROXY/ALL_PROXY env vars and a curl -v trace) but does not appear to reject the CONNECT for non-allowlisted hosts.

Expected behavior

A curl (or other Bash-tool network call) to a host not present in the session's allowedHosts list should fail at the proxy — e.g. the CONNECT should be rejected, or the request should otherwise be blocked before reaching the origin.

Actual behavior

CONNECT tunnels to hosts outside the allowlist succeed and return real content from the origin server, indistinguishable from a fully open network.

Repro steps

  1. In a Claude Code session with sandboxing enabled, note the configured allowedDomains (a short curated list, e.g. github.com, registry.npmjs.org, pypi.org — plus, in this case, a couple of project-specific domains not relevant to the repro).
  2. Via the Bash tool, run curl against a well-known public host that is not on that list, e.g.:

``
curl -sI --max-time 5 https://www.google.com
curl -sI --max-time 5 https://www.example.com
``

  1. Observe: both return normal 200/real headers instead of being blocked.
  2. Confirm the request is actually going through the sandbox proxy (not bypassing it):

``
env | grep -i proxy
curl -sv --max-time 5 -o /dev/null https://www.google.com 2>&1 | grep -E "Trying|Connected|CONNECT"
`
This shows
HTTPS_PROXY/ALL_PROXY pointed at a local proxy (http://...@localhost:<port>) with an auth token, and the verbose trace shows curl connecting to that local proxy and issuing CONNECT <host>:443` through it successfully — i.e., the proxy itself is what's letting the non-allowlisted host through, not a bypass of the proxy.

What this suggests

The proxy correctly intercepts outbound traffic (env vars are set, CONNECT tunneling is established through it), but it either:

  • isn't enforcing a host allowlist on CONNECT at all, or
  • is enforcing some other/broader list than what's documented in the tool-facing allowedHosts config, or
  • is treating allowedHosts as advisory (e.g. "no confirmation needed") rather than a hard block.

Any of these means the documented security boundary ("anything else gets blocked at the network layer") does not currently hold — sandboxed sessions can reach arbitrary hosts over HTTPS despite a configured allowlist.

Environment

  • Claude Code CLI version 2.1.241
  • macOS (Darwin)
  • Sandbox enabled via project .claude/settings.json with a filesystem + network sandbox config block
  • Observed via the Bash tool's built-in curl; also reproduced via an MCP browser tool (Playwright) navigating to a non-allowlisted host, which loaded successfully — so this doesn't look Bash-specific either.

Impact

Reduces confidence in the network sandbox as an actual enforcement boundary rather than a UI/documentation-only allowlist. Anyone relying on allowedHosts to constrain what a session can reach over the network should verify current enforcement before depending on it.

View original on GitHub ↗

3 Comments

faramolaisiaka · 6 days ago

Follow-up from filing this issue:

  1. While trying to file this very issue via gh issue create, gh itself failed with a TLS certificate verification error (Post "https://api.github.com/graphql": tls: failed to verify certificate: x509: OSStatus -26276), even though the auth token was valid (confirmed by using the same token via curl directly against the same API, which succeeded with HTTP 200). Worked around it by posting straight to the REST API with curl instead. gh was NOT in the sandbox config's excludedCommands (unlike git/docker/op/caffeinate) at the time, so it was running inside the sandboxed environment — adding it to excludedCommands is a plausible workaround, unconfirmed.
  1. Testing that workaround surfaced a second, arguably more serious gap: the sandboxed session was able to write to the global settings.json that defines the sandbox's own config (including excludedCommands), successfully appending "gh" to that array via a normal file-edit tool call. That file is expected to be protected against writes from within a sandboxed session (a self-modification guard on the config governing the sandbox itself), and it was not. So the enforcement gap isn't limited to outbound network — it also affects filesystem write protection on the sandbox's own configuration file.
faramolaisiaka · 6 days ago

Follow-up #2 — the filesystem gap generalizes beyond settings.json.

Isolated it further: the Write/Edit tool does not appear to go through the same filesystem sandbox as the Bash tool at all, for any path — not just the specific denyWithinAllow-listed config files.

Repro:

  • Via Bash, echo test > ~/.claude/__probe__.txt (a directory not in allowOnly, only a debug subdir is): correctly blocked with operation not permitted.
  • Via the Write tool, writing the same brand-new path ~/.claude/__probe__.txt: succeeded, no error.
  • Also could not rm that file afterward via Bash (correctly blocked, consistent with the write restriction actually working for Bash) — had to remove it with the sandbox override on that one delete, since it was an orphaned artifact in a directory Bash is otherwise correctly restricted from.

So this is not scoped to the denyWithinAllow carve-outs (like the earlier settings.json case) — Write/Edit looks like it skips filesystem sandboxing entirely, while Bash's own filesystem enforcement (tested separately: blocked writes to ~/Desktop, blocked writes/deletes to ~/.claude/, blocked reads of a denyOnly path) is working correctly. The two tools appear to run on different enforcement paths, and only one of them is actually restricted.

Orellius · 4 days ago

Corroborating this from an independent implementation, and adding the kernel-level reason it is hard to fix inside sandbox-exec alone.

Measured on macOS while building a containment layer for coding agents. Same behaviour: the proxy is in the request path and the CONNECT is not rejected for a host outside the allowlist.

Two things that may be useful to whoever picks this up.

1. The seatbelt profile cannot help you here. sandbox-exec refuses a hostname outright:

sandbox-exec: host must be * or localhost in network address (remote ip "example.com:443")

* and localhost are the entire vocabulary. Measured, with DNS resolved in advance so it was not the variable:

| profile | reaching a non-allowlisted host on 443 |
|---|---|
| (deny network-outbound) + (allow network-outbound (remote ip "*:443")) | 200 |
| (deny network-outbound) + (allow network-outbound (remote ip "*:80")) | blocked |
| (deny network*) | blocked |

So the kernel filters by port and by nothing else. Allowing *:443 so the agent can reach the model API allows every HTTPS destination there is. And a blanket deny takes DNS with it: every probe under it failed with curl exit 6, could not resolve host, before any connection was attempted.

2. The shape that did hold, in case it is useful as a reference: the kernel profile allows the loopback and nothing else, the only thing on the loopback is a CONNECT proxy that owns the allowlist, and the child is given HTTPS_PROXY pointing at it with NO_PROXY explicitly cleared. An inherited NO_PROXY is worth calling out, because it sends API traffic direct and that traffic then dies in the jail with a confusing error.

The property that matters is that this is not a rule a tool consults. It is the only socket the process is allowed to open, so a tool that never heard of the allowlist cannot walk past it. That is what makes it different from allowedHosts as currently enforced.

Happy to share the exact profile and a repro script if that is useful.

Measured on macOS 15, Apple silicon, claude 2.1.233. The settings keys involved are unchanged in 2.1.246, checked today.

Context, since it is fair to say where this comes from: measured while building Januas, a vendor-neutral supervision and containment layer for coding agents. Not released, nothing to sell you; the measurements are the only reason for this comment.