Sandbox read-denies .git/config.worktree, breaking all git commands in repos with extensions.worktreeConfig=true
## ⚠️ AI-assisted report — disclosure This issue was investigated and written with AI assistance (Claude Code, model Claude Opus 4.8). Specifically: 1. Investigation: The root cause was diagnosed empirically in a live Claude Code session driven by the AI — inspecting the/dev/nullbind mount over.git/config.worktree, testing whethersandbox.filesystem.allowReadoverrides the mask, and confirming git's config-load failure. A separate AI research subagent then searched the web and theanthropics/claude-codeissue tracker to find related reports and rule out duplicates. 2. Report authoring: This issue description was drafted by the AI from those findings. A human (the account filing this issue) reviewed and approved the content before submitting. The empirical claims were reproduced on a real system (environment and steps below); please still verify independently.
Summary
On Linux (bubblewrap), the sandbox masks .git/config.worktree (and .git/config.lock) with a read-denied /dev/null bind mount. In any repository that has the extensions.worktreeConfig=true extension enabled — which git worktree and git sparse-checkout enable automatically — git must read .git/config.worktree during config load, so every git invocation fails, including read-only ones like git status / git log / git diff:
warning: unable to access '.git/config.worktree': Permission denied
warning: unable to access '.git/config.worktree': Permission denied
fatal: unknown error occurred while reading the configuration files
The documented behavior is that the sandbox denies only writes to .git/config and .git/hooks in a worktree's shared .git while allowing git commit to update refs/index (see the "Git worktrees" bullet under Filesystem isolation in the sandboxing docs). The observed behavior additionally read-denies config.worktree, which is what breaks git entirely for worktree-config repos.
Environment
- Claude Code 2.1.217
- git 2.43.0
- bubblewrap 0.9.0
- Linux (Ubuntu), sandbox enabled,
filesystem.denyRead: ["/"]with an explicitallowReadallowlist (repro does not depend on deny-root; see below)
Reproduction
- In a repo that uses linked worktrees (so
extensions.worktreeConfig=true):
````
git config --get extensions.worktreeConfig # -> true
- Enable the sandbox and run any git command inside the sandbox:
````
git status
- Observe the
unable to access '.git/config.worktree': Permission deniedfatal above.
stat shows the file has been replaced by a /dev/null character device owned by nobody:nogroup, created at sandbox/session start:
crw-rw-rw- 1 nobody nogroup 1, 3 .git/config.worktree
What does NOT fix it
sandbox.filesystem.allowReadfor the exact file path — verified it does not override the mask; the read still returnsPermission deniedand the file is still the/dev/nulldevice. The.gitconfig/hooks protection sits above theallowReadlayer.git -c extensions.worktreeConfig=false .../GIT_CONFIG_GLOBAL=/dev/null— git readsconfig.worktreeduring initial config load, before-c/env overrides apply.- Disabling
extensions.worktreeConfigon the repo — unsafe when worktrees/sparse-checkout exist:config.worktreeholds per-worktreecore.worktree/core.bare/core.sparseCheckoutthat git documents as "must not be shared". Disabling breaks those setups, and Claude Code's own worktree feature re-enables it.
The crux (please confirm)
A /dev/null --ro-bind should make a file read as empty (0 bytes), not return EACCES. Here reads get Permission denied, and that read failure is what kills git. If the masked .git config files were instead empty-but-readable, read-only git (and likely git commit, per the documented worktree write-allowance) would work inside the sandbox with no escape hatch needed. The extra read-deny appears to be the actual defect.
Impact
Anyone using git worktree (increasingly common, and used by Claude Code's own worktree feature) cannot run any git command in the sandbox — not even read-only ones — despite the docs implying git works in worktrees. The only workarounds fully unsandbox git.
Current workarounds (all imperfect)
sandbox.excludedCommands: ["git *"]— works, but a glob match unsandboxes the entire shell line (e.g.git x && <evil>runs<evil>unsandboxed too), per #40831 (closed not-planned). Also fully unsandboxes git (fs + network).allowUnsandboxedCommands: true+ per-commanddangerouslyDisableSandboxretry — classifier-gated per invocation, avoids the standing wildcard, but every git command does a failed-then-retry round trip.sandbox.filesystem.disabled: true— fixes git but drops all filesystem isolation, includingdenyReadandcredentials.files.
Suggested fixes (in preference order)
- Present masked
.gitconfig files (config,config.worktree,config.lock) as empty-readable instead of read-denied, so git's config load succeeds while writes stay blocked. This keeps git sandboxed and functional in worktrees. - Alternatively, expose the sandbox-runtime
allowGitConfigflag assandbox.filesystem.allowGitConfig(feature request #58178) and ensure it also permits the read ofconfig.worktree. - At minimum, document that repos with
extensions.worktreeConfig=truecannot run git in the sandbox, and why.
Related issues / references
- #25603 (mechanism: bwrap
/dev/nullbind artifacts break git), closed as dup of #17087 / #17727 - #29316 (empty stub files created when
.gitis a file / worktree) - #22320 (
mkdir .git/hooksfails when.gitis a file / worktree) - #33657 (
.git/worktrees/<name>/not in write allowlist) - #13195 (
Edit(.git/**)doesn't let git write.git/configin sandbox; maintainer workaround = excludedCommands) - #58178 (feature request: expose
allowGitConfig; stale) - #40831 (excludedCommands glob unsandboxes the whole shell line; closed not-planned)
- Docs: sandboxing — Git worktrees
- git docs: git-worktree CONFIGURATION FILE, git-config
extensions.worktreeConfig
This config.worktree-specific root cause does not appear to be captured in the existing issues above — it sharpens #25603 / #29316 by identifying the exact trigger (the worktreeConfig extension) and the read-deny-vs-empty-read distinction.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗