[BUG] Sandbox kills every Bash call in worktrees at the default `.claude/worktrees/` location (bwrap EROFS creating protected-path mountpoints)
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?
With the built-in Bash sandbox enabled, a session running in a worktree that Claude Code itself created at the default location <repo>/.claude/worktrees/<name> cannot run any Bash command — every call dies while bwrap builds the sandbox, before the user command starts. Edit/Write tools still work; only sandboxed Bash is dead, which makes the worktree session unusable.
Mechanism, consistent with every experiment I ran (details in Additional Information):
- The sandbox write-denies protected paths of the session's checkout:
<checkout>/.github,<checkout>/.gitconfig,<checkout>/.mcp.json,<checkout>/.claude/..., etc. - A non-existent denyWrite path is enforced by having bwrap create the mountpoint and ro-bind over it, and this creation is fail-closed (the same asymmetry documented in #79997 — missing read-allow paths are skipped, missing write-deny mountpoints abort the sandbox).
- These protected paths are usually absent from a fresh worktree checkout (
.gitconfigand.mcp.jsonare virtually always untracked;.claude/and.githuboften are). - The worktree lives inside
<repo>/.claude, which the sandbox has already ro-bound as a protected path of the main checkout. Mountpoint creation inside the worktree therefore hits EROFS and bwrap aborts.
The permission-guard layer already exempts .claude/worktrees/ from protected-path treatment (mentioned in #80721), but the bwrap profile builder has no such exemption — so Claude Code's own default worktree location conflicts with Claude Code's own sandbox.
What Should Happen?
A session in a worktree that Claude Code created at its own default location should be able to run sandboxed Bash commands.
Either of these would fix it (both seem worth doing):
- Carve the session's worktree subtree out of the parent checkout's
.claudeRO bind, mirroring the guard-layer exemption of.claude/worktrees/, so mountpoints inside the worktree can be created. - Make write-deny mountpoint creation fail-open (skip the bind when the mountpoint cannot be created under an unwritable parent), as proposed in #79997 — the kernel already denies writes there, so the skipped bind loses no protection.
Error Messages/Logs
bwrap: Can't create file at /home/<user>/tools/<repo>/.claude/worktrees/<name>/.github: Read-only file system
# After pre-creating <worktree>/.github by hand, the failure just moves to the next missing protected path:
bwrap: Can't create file at /home/<user>/tools/<repo>/.claude/worktrees/<name>/.gitconfig: Read-only file system
Steps to Reproduce
- Use a git repo where at least one protected path (
.github,.gitconfig,.mcp.json,.claude/) is absent from a fresh checkout — i.e. nearly any repo. In my repro,.gitignorecontains.claude/,.mcp.json, and.github. - Enable the Bash sandbox (bwrap) and trust the workspace:
````
cd ~/some-repo
claude # sandbox enabled: sandbox.enabled=true, failIfUnavailable=true
- Start a worktree session at the default location:
````
claude --worktree test
- Ask for any Bash command in the session — or reproduce headlessly:
````
cd .claude/worktrees/test
claude -p 'Use the Bash tool to run: ls -la'
- Every Bash call fails with
bwrap: Can't create file at .../.claude/worktrees/test/.github: Read-only file system.
Reproduced in two unrelated repos on the same machine. The main checkout of the same repos runs sandboxed Bash fine.
Claude Model
Not sure / Multiple models
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.227 (Claude Code)
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
WSL (Windows Subsystem for Linux)
Additional Information
Environment: WSL2 — Ubuntu 24.04.4 LTS, kernel 6.6.87.2-microsoft-standard-WSL2, bubblewrap 0.9.0. Sandbox settings: enabled: true, failIfUnavailable: true, allowUnsandboxedCommands: false, network strictAllowlist: true.
What I ruled out:
- Pre-creating the missing mountpoints (
touch <wt>/.github <wt>/.mcp.json; mkdir <wt>/.claude): the failure just moves to the next missing protected path (.gitconfig), confirming the whole worktree subtree is read-only at profile-build time — it cannot be fixed one path at a time. sandbox.filesystem.allowWrite: ["<repo>/.claude/worktrees"]in user settings: no effect; an allow entry cannot undo the ancestor's RO deny-bind.- A worktree outside
.claude/(git worktree add ../repo-test+claudein it): works perfectly in the same repo — Bash runs, and the protected paths are correctly masked (visible ascrw-rw-rw-character devices), proving mountpoint creation succeeds as soon as the parent chain is writable. .git-as-file (#22320): different failure there (Can't mkdir parents for .../.git/hooks: Not a directory); that issue is closed as duplicate and my path/errno differ.- WSL2 userns/AppArmor problems: not the cause; the sandbox works fine in the main checkout of the same repos.
Related issues (all distinct from this one): #79997 (same fail-closed denyWrite-mountpoint creation, triggered by root-owned ancestors instead of worktrees), #22320 (closed; .git-as-file), #83922 (unconditional .mcp.json ancestor binds), #80721 (notes the .claude/worktrees exemption at the guard layer).
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗