anthropics / anthropics/claude-code
Bash sandbox denies writes to <repo>/.claude/ paths, silently corrupting the git working tree in repos that track files there
- Lingua principale
- Python
- Stelle
- 145k
- Fork
- 23.1k
- Metriche di merge delle PR
- Metriche PR in attesa
Descrizione
## Summary
The Bash sandbox denies writes under `/.claude/skills`, `/.claude/hooks`, and `/.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.:
```
/.claude/skills
/.claude/settings.json
/.claude/hooks
```
These are treated as agent configuration. In this repo they are **version-controlled source files**. Nothing reconciles the two.
Verified directly:
```console
$ touch /.claude/skills/sandbox-write-probe
touch: /.claude/skills/sandbox-write-probe: Operation not permitted
$ touch /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:
```console
$ git switch -c my-branch origin/some-branch
warning: unable to unlink '.claude/skills//SKILL.md': Operation not permitted
warning: unable to unlink '.claude/skills//SKILL.md': Operation not permitted
... (12 files)
error: could not lock config file /.git/config: Operation not permitted
error: unable to write upstream branch configuration
```
3. `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.
4. A plain `git switch ` 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 `/.git/config` also fail with `Operation not permitted`, even though `/.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.
Guida per i contributori
Nessuna guida per i contributori indicizzata per questo repository
Direzione di ricerca
Start by reproducing the checkout in a repository where git ls-files includes .claude/skills files, then trace the sandbox denyWithinAllow handling and the Bash tool's treatment of git failures. Check how writes to .git/config are handled as well. Done means tracked .claude paths no longer break checkout, or the invocation fails loudly with an inconsistency warning, without misleading branch or upstream state.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Valutazione
- Stack tecnologico
- bash, git
- Ambito
- developer-experience, devtools, security
- Tipo di issue
- Bug
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Stato di attività
- Attiva
- Chiarezza
- Abbastanza chiara
- Idoneità per principianti
- 48/100