Sandbox masks nonexistent deny-listed paths as unreadable device nodes - breaks plain git whenever `extensions.worktreeConfig` is enabled
Environment: Claude Code on WSL2 (Linux 6.6.x, Ubuntu), default sandbox enabled.
Behaviour. The sandbox's write-deny list for a project includes .git/config, .git/config.worktree, .git/config.lock, .git/hooks. Inside the sandbox namespace, .git/config.worktree is presented as a device node (crw-rw-rw- 1 nobody nogroup 1, 3) even when the path does not exist on the real filesystem, and opening it fails with EPERM - so the deny blocks reads, not just writes. Notably .git/config (a real file) stays readable in-sandbox; only the nonexistent deny-listed path gets the unreadable mask.
Impact. Git reads .git/config.worktree on every command once extensions.worktreeConfig=true is set in .git/config - and tooling (including worktree-isolation features) can leave that flag behind after the last worktree is removed. From then on every plain git command in a sandboxed shell dies:
warning: unable to access '.git/config.worktree': Permission denied
fatal: unknown error occurred while reading the configuration files
Users see "git is broken in the sandbox" with no obvious cause, and the standard workarounds (disable sandbox per-command, or gh --repo to avoid local git) hide the diagnosis. A second, cosmetic effect: with git working, sandboxed git status lists the other injected masks (.bashrc, .gitconfig, .idea, .claude/agents, ...) as phantom untracked files.
Expected. Reads of deny-listed paths should behave like the real filesystem (ENOENT for nonexistent paths, normal reads for existing ones) while writes stay denied - i.e. a read-transparent write guard. That would keep the tamper-guard intact and let plain git work regardless of extensions.worktreeConfig.
Repro.
- In a sandboxed project,
git config extensions.worktreeConfig true(no worktrees needed). - Run any sandboxed
git log/git status- fatal as above. ls -la .git/config.worktreein-sandbox shows the device node; outside the sandbox the path does not exist.git config --unset extensions.worktreeConfig- sandboxed git works again.
4 Comments
This has a pretty significant impact for my work as well. And, in my work, I've concluded that the bug impacts me because I'm using sparse checkouts.
Noting that here, in case it helps with fixing this defect. In my installation, Claude Code self diagnosed this conflict (of sandbox and worktreeConfig) as _only_ impacting situations where some of the worktrees use sparse checkout. The use of sparse (according to Claude) is what prevents me from clearing the extension from the git config in my root repo.
An excerpt from my Claude conversation:
Same mechanism, reproduced on native Linux (not WSL2), with two failure modes beyond the config-read path described above.
Environment: Claude Code 2.1.221, Linux 7.x native, git 2.55.0, default sandbox enabled.
Same root cause, different deny-listed path. The sandbox masks
.gitmodulesat the session working-directory root with the identical node:statreportscharacter special file, device type1,3(=/dev/null). The path does not exist on the host filesystem.Failure mode 1 —
git addaborts, not only config reads. Git also refuses to index a character device:So the blast radius covers staging and committing, not just configuration parsing.
Failure mode 2 — masked paths that the repository tracks. At the working-directory root the mask list includes
.claude/settings.json,.claude/settings.local.json,.claude/skills,.claude/hooks,.claude/agents,.claude/commands,.mcp.json,.env,.gitconfig,.gitmodules, the shell rc files,.vscodeand.idea. A repository that versions any of them — for example a project that ships its own.claude/skills/directory — has those tracked files replaced by a device node for the duration of the session, and git reports them as deleted.Why it is hard to attribute.
git status,git diffandgit submodule statusnever read.gitmodules, so the working tree looks clean and the breakage only surfaces atgit add. Tooling that shells out to git reports it as its own defect: in our case a review CLI returnedgit_command_failedfrom a pre-flight phase, which sent us auditing that tool instead of the sandbox.Scope observed. The mask is applied only at the session working-directory root, never recursively — launching from a subdirectory of the repository leaves the repository root clean.
sandbox.filesystem.allowReaddoes not override it; the bind mount is a separate layer and wins. The only setting that removes it issandbox.filesystem.disabled: true, which turns off filesystem isolation entirely.Suggested narrowing. When a deny-listed path does not exist on the host, creating the mount point has no protective value — there is nothing to hide — and it converts a clean absence, which git ignores silently, into a hard failure. Skipping mount-point creation for nonexistent paths would close this class without weakening protection for paths that do exist.
Running with idea from @AlejandroTatum above I was able to work around the problem. Workaround that doesn't need
sandbox.filesystem.disabledor unsetting the extension: create an empty regular file at the deny-listed path. The mask only materializes where the file is absent on the host — deny-listed files that exist are left readable, and an emptyconfig.worktreeis semantically identical to no file (git reads it, finds nothing).Evidence from my repo, 14 worktrees, one
ls: the 8 sparse ones each have a real 57-byteconfig.worktreeand are readable in-sandbox with git working normally; the 6 non-sparse ones have no host file, showcrw-rw-rw- nobody nogroup 1, 3, and every git command fails. Same deny-list entry, opposite behaviour, decided purely by host existence.This is just a hack that worked for me. sharing it here for reference. For me, with sandbox and worktrees and some worktrees that are sparse, this got me sorted out.
Confirmed — reproduced on 2.1.233 (Linux, default sandbox), and this is a regression since 2.1.193.
What I ran: fresh repo,
git config extensions.worktreeConfig true, no.git/config.worktreeon disk, then had Claude rungit statusandls -la .git/config.worktree; cat .git/config.worktreein the sandboxed Bash tool.Observed exactly what you describe: inside the sandbox the nonexistent
.git/config.worktreeshows up as an unreadable device node (crw-rw-rw- 1, 3), reads fail withPermission denied, and every git command dies withfatal: unknown error occurred while reading the configuration files. With the sandbox off, withextensions.worktreeConfigunset, or when the file actually exists, everything works — only the nonexistent protected path gets the unreadable mask.The write-protection on that path is intentional hardening, but presenting a nonexistent protected path as an unreadable placeholder — turning a write-deny into a read failure — is not. Versions 2.1.181–2.1.191 carry the same protection without the breakage; it regressed in 2.1.193. We'll track a fix, e.g. making the placeholder read as an empty file (git tolerates an empty
config.worktree) or not materializing it at all.Workaround until then: create the file (
touch .git/config.worktree) — a real file stays readable and git works normally inside the sandbox.🤖 Generated with Claude Code