[BUG] [claude-generated] sandbox filesystem: allowRead re-bind of a directory is shadowed by a later --tmpfs on a parent path
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
(This bug report is co-authored by a LLM. I hope this is acceptable for this project.)
When running plain claude (`~/.config/Claude/claude-code/2.1.209/claude, no claude-desktop), sandboxed bash commands do not see the allow-within-deny exception due to a wrong ordering of the bindings in bwrap. (/u/ and /u/pklenze` are different file systems, not that this should matter).
Rest of this post is claude-authored.
---
What's Wrong?
With a config like:
{
"sandbox": {
"enabled": true,
"filesystem": {
"denyRead": ["/u/"],
"allowRead": ["/u/pklenze/claude_work/"]
}
}
}
allowRead is supposed to re-open /u/pklenze/claude_work/ inside the denyRead'd /u/ region (per the docs: "the more specific path wins"). This works for individual files, but entire directories bound via allowRead are silently emptied, because of bwrap argument ordering.
Captured from ps during a session, the generated invocation includes (order preserved, trimmed):
--bind /u/pklenze/claude_work /u/pklenze/claude_work
--bind /tmp/claude-8001/ /tmp/claude-8001/
--tmpfs /u/
--ro-bind /dev/null /u/pklenze/claude_work/.claude/settings.json
--ro-bind /u/pklenze/claude_work/.claude/settings.local.json /u/pklenze/claude_work/.claude/settings.local.json
... (more individual --ro-bind file re-exposes)
--ro-bind /u/pklenze/claude_work/drasi/.git/config /u/pklenze/claude_work/drasi/.git/config
--ro-bind /u/pklenze/claude_work/epics/epicscorelibs/.gitmodules /u/pklenze/claude_work/epics/epicscorelibs/.gitmodules
--ro-bind /u/pklenze/claude_work/epics/pythonSoftIOC/.gitmodules /u/pklenze/claude_work/epics/pythonSoftIOC/.gitmodules
bwrap applies mounts in argument order, and a later mount on a parent path shadows everything mounted under it earlier. Here, --tmpfs /u/ is emitted after the --bind that should satisfy allowRead, so the entire claude_work bind is wiped. Only the small set of individual files Claude Code re-binds one-by-one after the tmpfs (mostly its own config, plus a few auto-detected git files) survive. Everything else under the allowed directory — including epics/pythonSoftIOC/.git/ in its entirety — becomes inaccessible with No such file or directory, even though denyRead/allowRead overlap resolution should make it readable.
Environment
- OS: Linux
- Sandbox: bubblewrap-based,
sandbox.enabled: true,sandbox.filesystem.denyRead+allowRead
What Should Happen?
The bind for a directory covered by sandbox.filesystem.allowRead should be emitted after any --tmpfs mount on an ancestor path (or the tmpfs/deny mounts should be emitted first, then all allow re-binds applied on top), so the full directory tree under the allowed path is actually restored — not just the individually-curated files Claude Code happens to re-bind for its own config protection.
Error Messages/Logs
Steps to Reproduce
- Config:
``json``
{
"sandbox": {
"enabled": true,
"filesystem": {
"denyRead": ["/u/"],
"allowRead": ["/u/<user>/claude_work/"]
}
}
}
- Have a git repo somewhere under
/u/<user>/claude_work/, e.g./u/<user>/claude_work/epics/pythonSoftIOC/.git. - Start a Claude Code session, run a sandboxed Bash command, e.g.
ls -la /u/<user>/claude_work/epics/pythonSoftIOC/.git. - Observe:
No such file or directory, despite the path being nested inside theallowReaddirectory and existing on the real filesystem with normal permissions. - Inspect the actual
bwrapinvocation (e.g. viaps auxwhile a sandboxed command briefly runs) and note the--tmpfs /u/argument appears after the--bind /u/<user>/claude_work ...argument.
Claude Model
Sonnet (default)
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.209
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
Xterm
Additional Information
This is a distinct root cause from #25603 and #17727 (which are about wrong paths being targeted for git-internal protection artifacts). This is about mount ordering nullifying a legitimate allowRead directory re-open entirely.
3 Comments
Update: the reason I do not encounter this bug in claude-desktop is because when run from claude-desktop, claude runs bash commands entirely without bwrap, which I also consider surprising behavior.
I now have a workaround.
My idea was to run the claude binary itself in bwrap to restrict access to /u/. That way I do not need to use `
~/.claude/settings.json` to restrict file system access -- if claude can not read my home directory, then any processes it spawns will also not be able to do that.Together with `
"denyRead":[]`, this does what I want to do, though I still needed to lock down access to .claude/credentials from within the sandbox.It would still be nice if the bug got fixed.
Thanks — capturing the actual mount argument order made this a very actionable report, and your analysis of the ordering semantics is correct.
I tested this on the current release (2.1.233) on Linux with an equivalent config (
denyReadon a parent directory,allowReadon a subdirectory containing the working directory) and inspected the actual sandbox invocation:allowReaddirectory is emitted after the read-write bind of the working directory nested inside it, shadowing the working directory read-only. Later sandbox-setup mounts that need to create files directly in the working directory then fail withbwrap: Can't create file at .../.mcp.json: Read-only file system— so every sandboxed command errors out at sandbox setup with this deny/allow shape. Unlike the silent empty directory you saw, this at least fails loudly, but theallowRead-inside-denyReadconfiguration remains unusable when the working directory sits inside the allowed region.So: original symptom likely fixed since 2.1.209 (upgrading is worth trying), but keeping this open for the remaining ordering issue — mounts for allowed regions need to be applied ancestors-first so more specific binds aren't shadowed by their parents.
If you upgrade and still see the silent empty directory variant rather than the loud setup error, please post the captured mount ordering again along with your exact version.
🤖 Generated with Claude Code