Sandbox masks nonexistent deny-listed paths as unreadable device nodes - breaks plain git whenever `extensions.worktreeConfig` is enabled

Status Open
Maintainer reply ✓ Yes — bcherny
Activity 4 comments · opened Jul 11, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

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.

  1. In a sandboxed project, git config extensions.worktreeConfig true (no worktrees needed).
  2. Run any sandboxed git log / git status - fatal as above.
  3. ls -la .git/config.worktree in-sandbox shows the device node; outside the sandbox the path does not exist.
  4. git config --unset extensions.worktreeConfig - sandboxed git works again.

View original on GitHub ↗

4 Comments

wahuneke · 1 month ago

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:

.... The conclusion, in one causal chain: 1. You (via claude --worktree with worktree.sparsePaths) create sparse worktrees. Git sparse-checkout stores its per-worktree path list in config.worktree, and to do that git automatically flips on extensions.worktreeConfig = true in the shared .git/config. 2. That extension makes config.worktree a mandatory config read for every worktree — including the main checkout (.git/config.worktree), which is why the error appears even when you're standing in the root repo, not a worktree. It's not that git thinks you're in a worktree; the extension globally promotes that file to a required source. 3. The Claude Code sandbox's scrub-mode plants a /dev/null stub over the .git/config* family (they're on the deny-within-allow write list) so sandboxed processes can't mutate git config. Reads of that stub return Permission denied. 4. ....
AlejandroTatum · 25 days ago

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 .gitmodules at the session working-directory root with the identical node:

crw-rw-rw- 1 nobody nobody 1, 3  .gitmodules

stat reports character special file, device type 1,3 (= /dev/null). The path does not exist on the host filesystem.

Failure mode 1 — git add aborts, not only config reads. Git also refuses to index a character device:

$ git config -f .gitmodules --list
fatal: unable to read config file '.gitmodules': Permission denied

$ git add -A
fatal: .gitmodules: can only add regular files, symbolic links or git-directories

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, .vscode and .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 diff and git submodule status never read .gitmodules, so the working tree looks clean and the breakage only surfaces at git add. Tooling that shells out to git reports it as its own defect: in our case a review CLI returned git_command_failed from 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.allowRead does not override it; the bind mount is a separate layer and wins. The only setting that removes it is sandbox.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.

wahuneke · 20 days ago

Running with idea from @AlejandroTatum above I was able to work around the problem. Workaround that doesn't need sandbox.filesystem.disabled or 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 empty config.worktree is 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-byte config.worktree and are readable in-sandbox with git working normally; the 6 non-sparse ones have no host file, show crw-rw-rw- nobody nogroup 1, 3, and every git command fails. Same deny-list entry, opposite behaviour, decided purely by host existence.

cd /path/to/repo/.git
[ -e config.worktree ] || : > config.worktree
for d in worktrees/*/; do [ -e "$d/config.worktree" ] || : > "$d/config.worktree"; done

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.

bcherny collaborator · 14 days ago

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.worktree on disk, then had Claude run git status and ls -la .git/config.worktree; cat .git/config.worktree in the sandboxed Bash tool.

Observed exactly what you describe: inside the sandbox the nonexistent .git/config.worktree shows up as an unreadable device node (crw-rw-rw- 1, 3), reads fail with Permission denied, and every git command dies with fatal: unknown error occurred while reading the configuration files. With the sandbox off, with extensions.worktreeConfig unset, 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