Bash sandbox denies writes to <repo>/.claude/ paths, silently corrupting the git working tree in repos that track files there
Summary
The Bash sandbox denies writes under <cwd>/.claude/skills, <cwd>/.claude/hooks, and <cwd>/.claude/settings.json — presumably to stop an agent rewriting its own configuration. When a repository tracks files at those paths, git can no longer check them out. Git operations then partially fail: the working tree is left inconsistent with HEAD, HEAD may not advance at all, and the result is reported as ordinary uncommitted modifications with no indication that a checkout failed.
A later session then inherits phantom "changes" it did not make, for which git records no author.
Environment
- Claude Code 2.1.255, desktop app (Code tab)
- macOS 15.7.9, arm64, git 2.50.1
- Sandbox enabled (
sandbox.enabled: true,autoAllowBashIfSandboxed: true) - A pnpm monorepo that tracks
.claude/skills/**as ordinary version-controlled content (~12 files), so the agent-skill definitions are shared with the team through the repo
Root cause
The sandbox's denyWithinAllow list includes repo-relative agent-config paths, e.g.:
<worktree>/.claude/skills
<worktree>/.claude/settings.json
<worktree>/.claude/hooks
These are treated as agent configuration. In this repo they are version-controlled source files. Nothing reconciles the two.
Verified directly:
$ touch <worktree>/.claude/skills/sandbox-write-probe
touch: <worktree>/.claude/skills/sandbox-write-probe: Operation not permitted
$ touch <worktree>/sandbox-write-probe # elsewhere in the same repo
(succeeds)
Reproduction
- Use a repo that tracks files under
.claude/skills/**. - From a sandboxed Bash tool call, switch to a branch whose
.claude/skillscontent differs from the current branch's:
$ git switch -c my-branch origin/some-branch
warning: unable to unlink '.claude/skills/<skill-a>/SKILL.md': Operation not permitted
warning: unable to unlink '.claude/skills/<skill-b>/SKILL.md': Operation not permitted
... (12 files)
error: could not lock config file <repo>/.git/config: Operation not permitted
error: unable to write upstream branch configuration
git rev-parse --abbrev-ref HEADstill reports the old branch, while the working tree has been updated — in my case 488 files then showed as staged changes.- A plain
git switch <branch>afterwards recoversHEAD.
Note that step 2 exits in a way that reads as success apart from the warning: lines.
Evidence this bites in the wild
A session was handed a fresh worktree that was already checked out to another session's branch, carrying 5 modified files — all of them under .claude/skills/, i.e. exactly the denied path. Alongside that:
- The branch matching the worktree's own name existed and was unused (zero unique commits vs the default branch), so the intended checkout had evidently not completed.
- The 5 modified files were byte-identical to their content at an older commit, while
HEADsat ~10 commits later. - Files added to that directory after that older commit were untouched.
- No session transcript on the machine recorded any edit to those paths — because no edit was ever made.
- The worktree that originally owned the branch had already been pruned; its branch and its dirty tree survived it, and its transcript was gone, so there was no provenance left to recover.
Impact
- Silent working-tree corruption. The only signal is a
warning:line inside one tool call. Afterwards it is indistinguishable from ordinary uncommitted work. switch -cleavesHEADbehind, so tree andHEADdisagree. This had been a recurring, unexplained trap in this repo.- False provenance, and this is the serious one. A later session sees authored-looking modifications that have no author. In our case the session committed them with an invented rationale ("WIP: park in-progress spec-skill edits") and then came close to proposing them to a colleague as a deliberate rollback of an already-merged feature. Had that been accepted without close review, it would have reverted shipped functionality — plus a second, later feature that happened to touch the same files. Roughly two hours went into working out that the diff was an artifact and not a decision.
The failure mode is that this bug manufactures plausible-looking, unattributable diffs.
Expected behaviour
Either:
- the sandbox does not deny paths that git needs in order to check out tracked files; or
- a denied write inside a git invocation surfaces as a hard, unmissable failure that says the working tree may now be inconsistent with
HEAD— rather than a warning followed by a half-updated tree.
Suggested fixes, in preference order
- Don't deny
.claude/paths that the repo tracks. Reconcile the deny list againstgit ls-files: a version-controlled file is repo content, not agent config. - Scope the protection to what it is for. The concern is an agent rewriting its own configuration. That is better enforced at the Edit/Write tool layer than by a filesystem deny that also blocks git's own plumbing.
- Fail loudly. If a sandbox deny causes a write failure inside a git invocation, report it as an error and state that the working tree may be inconsistent with
HEAD. - Guarantee clean worktree handover. A fresh session worktree should never arrive on another session's branch carrying another session's uncommitted changes — either create it clean, or refuse to start and say why.
Secondary issue (happy to split this out)
Writes to <repo>/.git/config also fail with Operation not permitted, even though <repo>/.git is on the sandbox allow list. Two consequences observed:
git push -upushes successfully but cannot record the upstream, so pushed branches read as unpushed (git for-each-refshows no upstream). That is independently misleading when auditing which local branches still hold unshared work.git branch -Ddeletes the ref but leaves stale config sections behind.