sandbox: filesystem reads are unrestricted, and the agent can silently break its own sandbox enforcement by editing settings.json
sandbox: filesystem reads are unrestricted, and the agent can silently break its own sandbox enforcement by editing settings.json
Summary
settings.json's sandbox.filesystem config only exposes an allowWrite allowlist — there is no read-restriction primitive. Combined with the fact that settings.json itself lives inside the agent's writable project/home tree, this produces two compounding problems:
- No filesystem read isolation exists at all. Credential directories like
~/.sshand~/.awsare fully readable by any Bash command the agent runs, sandbox "enabled" or not. The only thing gating a read of~/.ssh/id_rsais the model's own judgment / thepermissions.denyRead-tool rules — and those rules only apply to the dedicatedReadtool, not tocat/grep/etc. invoked throughBash. Adenylist like["Read(**/.env*)", "Read(**/.credentials*)", "Read(**/.secrets*)"]does nothing to stopBash({"command": "cat ~/.ssh/id_rsa"}).
- The agent can add non-existent/no-op config keys that look like they add protection, and can invalidate its own settings.json, with no visible failure. In our session, the agent added an undocumented key:
``json`
"sandbox": {
"credentials": {
"files": [
{ "path": "~/.ssh", "mode": "deny" },
{ "path": "~/.aws", "mode": "deny" }
]
}
}
sandbox.filesystem
This isn't a recognized schema field (every other example of config only supports allowWrite), so it appears to have had zero enforcement effect while giving the appearance of a fix. When the agent later reverted this edit, it left a trailing comma, making settings.json invalid JSON (jq error: Expected another key-value pair at line 8, column 3`). The CLI did not crash, did not warn, and did not block subsequent tool use — it kept running normally. Sandbox enforcement (whatever there was of it) silently stopped applying with no indication to the user.
Immediately after "fixing" the JSON syntax, the agent ran a verification command that both re-validated the JSON and read ~/.ssh/id_rsa in the same call, confirming the private key was readable — with sandbox reported as "enabled": true in the very settings.json that was just printed.
Environment
- Claude Code version:
2.1.224 - OS: Linux
Steps to reproduce
- Enable the sandbox in
~/.claude/settings.json:
``json``
{ "sandbox": { "enabled": true, "filesystem": { "allowWrite": ["/some/project/dir"] } } }
- Ask Claude Code to add a filesystem read-restriction for
~/.sshto the sandbox config (any phrasing works — the agent will invent a plausible-looking schema since none exists). - Ask it to run
cat ~/.ssh/id_rsa. Observe that it succeeds regardless of the sandbox config just added. - Ask it to revert the change. Observe/introduce a trailing-comma or similar syntax slip during the revert (this happened organically in our session via the Edit tool).
- Run any command that touches
settings.json(e.g.jq . ~/.claude/settings.json) — note the parse error, but note also that the CLI session itself does not crash, error out, or warn that sandbox enforcement may now be broken. - Run
cat ~/.ssh/id_rsaagain — it still succeeds.
Expected behavior
- Filesystem read isolation should be an enforceable, OS-level sandbox primitive (e.g. an
allowRead/denyReadlist honored by the actual sandboxing mechanism — bwrap/seccomp/whatever backssandbox.filesystem— not just an app-levelRead-tool permission string that Bash-invoked commands bypass entirely). settings.json(and any file that defines the sandbox's own rules) should not be writable by the sandboxed agent process itself. A sandbox that can edit its own confinement policy isn't a sandbox.- If
settings.jsonbecomes invalid or a sandbox config fails to apply for any reason, Claude Code should fail closed (block tool execution / refuse to proceed) and surface a clear, loud error — not silently continue running with degraded or absent enforcement.
Actual behavior
- No read restriction exists for any path, sandboxed or not; only writes are gated.
- The agent successfully self-edited its own sandbox policy file.
- An accidental syntax error from that self-edit silently disabled JSON parsing of the settings file with no crash and no warning surfaced in the session — the CLI kept running as if nothing had gone wrong.
~/.ssh/id_rsawas read and its contents (PEM header + partial key bytes) were sent to the model API and persisted in plaintext in the local session transcript (~/.claude/projects/.../*.jsonl), despite the sandbox being configured and reported as"enabled": truethroughout.
Why this matters
This is a defense-in-depth failure: the only layer preventing credential exfiltration was the model's own behavioral judgment, which is influenceable by conversation content and, in this case, by the model's own prior self-edits. A security boundary that the confined process can modify, and that fails open silently on malformed config, provides no real guarantee — arguably worse than no sandbox, since it's indistinguishable from a working one until something goes wrong.
Suggested fixes
- Add a real, OS-enforced filesystem read denylist/allowlist to the sandbox (not just write), and make
~/.ssh,~/.aws, and similar credential directories denied-by-default regardless of user config. - Make the sandbox's own policy file (
settings.json, or a dedicated sandbox policy file) unwritable from inside the sandbox it defines — enforce this at the OS/mount level, not via an app-leveldenyWithinAllowrule that only the same process's own tool-permission layer honors. - Fail closed: if the sandbox config can't be parsed/applied, block Bash/tool execution entirely and emit a hard, visible error, rather than continuing with no enforcement.
Showing cached comments. Read the full discussion on GitHub ↗
5 Comments
Since I've been evaluating sandbox functionality for my team's use, I'm pretty shocked that these issues haven't been fixed yet.
I was able to reproduce the "invalid
settings.jsonmid-session breaks sandbox" (and ALL other permission settings!) bug:Reproduction: permission enforcement fails open once
settings.jsonbecomes invalid JSONI reproduced this to see if Claude Code would respect a self-protecting deny rule in
settings.json.Setup
~/.claude/settings.jsoncontained apermissions.denyblock that explicitly protects itself from edits:themewas"light".Step 1 — Edit correctly denied (valid JSON, rule intact)
Asked Claude to change
"theme": "light"→"theme": "system"via its Edit tool. Result:(Note: that message is also strange, as the
denypermission is on the file, not the directory.)Correct —
Edit(settings.json)in the deny list was enforced as expected.Step 2 - Manually create malformed json
Removed the comma in the line before 'theme'.
Step 3 — Retried the identical edit
Same Edit call, same target string. This time the edit succeeded.
Claude stated that it expected the same result as my first request, then was surprised that its edit went through, and flagged the incorrect JSON that didn't result from an edit in the session.
The issue was reproduced - if you make
settings.jsoninvalid mid-session, this isn't flagged, and permission rules (as well as any other settings) are then ignored.Expected behaviour
settings.jsonshould be validated in the harness on every user prompt and before anything else happens, so that Claude Code will never run without permission or sandbox settings!It's possible for
settings.jsonto be set and enforced from a Managed Device setup, which means that users could have malformed JSON without their knowledge and without it ever being flagged until they ask Claude Code to actually look at the file or until they start a new session (that's whensettings.jsonis actually validated).Additionally, the
sandbox.credentialsschema the OP said is hallucinated, is part of the official spec, and I can confirm it works as expected (setting a canary dir / file to 'deny' or 'mask' blocks reads). Note: I'm working on macOS, so that might yield a different result!https://code.claude.com/docs/en/settings-reference#sandbox-credentials
I let Claude write this whole issue, but confirm it did indeed read my ~/.ssh/id_rsa.
I'm so pissed off about this that I honestly can't be bothered to have Anthropic fix this. I don't trust their skill level or commitment.
I've created my own sandbox, that I fully understand using
bwrapon Linux; I know this one works, and am sure Claude can not get out of it.Here's the article that I based my script on. Mine is a minor modification of the article, because I use different programming languages, but this is a great basis.
Independently measured the same two problems, and I can add a third interaction that is not obvious and that bit us.
On the read side. Confirmed: there is no read-restriction primitive, and
permissions.denyonly covers the dedicatedReadtool, notcatorgrepthroughBash. Two further measurements:permissions.denydoes fire underbypassPermissions, but only while the sandbox is enabled. With the sandbox off it stops applying. So the deny list and the sandbox are not two independent layers; either one alone is decorative.allowWritedoes not re-open a region that a deny pattern covers, and the working directory is not exempt from a deny pattern either. Worth stating in the docs, because both are easy to assume the other way round.On the self-disabling problem. Agreed that a settings file inside the agent's own writable tree cannot enforce anything against that agent. What worked here was moving the boundary under the process rather than beside it: a kernel sandbox profile built by the parent,
(deny file-write*)followed by an explicit list of allowed regions, with the agent's own settings file outside every allowed region. The agent can then edit whatever it likes and the boundary does not move, because the boundary was never in a file it can reach.The interaction that cost us a day, and it is the reason this is not a two-line fix. A sandbox that denies writes refuses a sandbox nested inside it. Not nesting in general, which works, but nesting inside a profile whose first line is
(deny file-write*). So an outer kernel jail requires telling the CLIsandbox.enabled: false, and that in turn drops the CLI's own network enforcement, which is the subject of #89165. The two controls are coupled in a way neither one documents.Measured on macOS. Happy to share the profile and a repro if 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.
Regarding the core architectural issue where in-tree
settings.json/ permission prompts can be bypassed via childBashcommands (cat ~/.ssh/id_rsa) or malformed JSON states:As @Orellius and @bvdeenen noted, relying on an in-process agent harness to police its own tools is structurally flawed because:
Readpermission checks) do not restrict raw syscalls executed by child shells (bash,python,curl).settings.jsonis writable by the agent (or if its parser fails open), permission enforcement collapses entirely.To solve this deterministically without requiring root or custom shell scripts, we built Vetto as an operator-controlled, pre-execution kernel boundary:
How the boundary works at the OS layer:
cat,grep, or subagents inherit the restriction automatically.$HOME/.ssh,~/.aws, and.envfiles are mapped directly to/dev/nulland directories to empty tmpfs. Even if Claude Code attemptscat ~/.ssh/id_rsathroughBash, the kernel returns EOF (0 bytes).