Bash sandbox denies writes to <repo>/.claude/ paths, silently corrupting the git working tree in repos that track files there

Status Open
Maintainer reply None cached
Activity 0 comments · opened Sep 9, 2026

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

  1. Use a repo that tracks files under .claude/skills/**.
  2. From a sandboxed Bash tool call, switch to a branch whose .claude/skills content 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
  1. git rev-parse --abbrev-ref HEAD still reports the old branch, while the working tree has been updated — in my case 488 files then showed as staged changes.
  2. A plain git switch <branch> afterwards recovers HEAD.

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 HEAD sat ~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

  1. Silent working-tree corruption. The only signal is a warning: line inside one tool call. Afterwards it is indistinguishable from ordinary uncommitted work.
  2. switch -c leaves HEAD behind, so tree and HEAD disagree. This had been a recurring, unexplained trap in this repo.
  3. 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

  1. Don't deny .claude/ paths that the repo tracks. Reconcile the deny list against git ls-files: a version-controlled file is repo content, not agent config.
  2. 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.
  3. 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.
  4. 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 -u pushes successfully but cannot record the upstream, so pushed branches read as unpushed (git for-each-ref shows no upstream). That is independently misleading when auditing which local branches still hold unshared work.
  • git branch -D deletes the ref but leaves stale config sections behind.

View original on GitHub ↗