[BUG] Sandbox git-internals write-protection resolves paths against the worktree root, not `.git/` — collaterally blocks a project's top-level `config/` dir
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
With the sandbox enabled, Claude Code adds write-deny rules to protect the repo's git internals. On a normal repository (.git is a directory, not a worktree pointer), those deny paths are built
against the worktree root instead of the resolved git directory — the .git/ segment is dropped.
The effective policy ends up with these write-deny entries:
````
/Users/me/repos/myproject/HEAD
/Users/me/repos/myproject/objects
/Users/me/repos/myproject/refs
/Users/me/repos/myproject/config
These are meant to be <repo>/.git/HEAD, <repo>/.git/objects, <repo>/.git/refs, <repo>/.git/config. Because the .git/ prefix is missing, the entries resolve to repo-root paths, which produces
two distinct problems:
- The protection misses its target.
HEAD,objects, andrefsdon't exist at the repo root, so those three entries are no-ops — the real.git/HEAD,.git/objects, and.git/refsstay writable. - Collateral damage to a legitimate project directory. The repo has a top-level
config/directory (this is a Rails app — every Rails app does). The bareconfigentry collides with it, so the
project's own config/ becomes write-blocked even though the repo root is in allowWrite.
None of these four entries appear in any settings file (managed, project, or user). They're synthesized at runtime by the bundled sandbox-runtime and applied in-process via the macOS Seatbelt API — there's
no profile file or settings key to inspect or override.
> Note: .git/config and .git/hooks are still correctly protected by a separate, properly-resolved built-in rule, so this is not a git-config/hooks security regression. The bug is (a) the four
worktree-root entries don't protect what they intend, and (b) they break writes to same-named project directories.
What Should Happen?
Git-internal deny paths should be resolved against the actual git directory — e.g. via git rev-parse --git-common-dir / --git-dir — yielding <repo>/.git/HEAD, <repo>/.git/config,
<repo>/.git/objects, <repo>/.git/refs (or the worktree's resolved gitdir).
A top-level project directory that happens to share a name with a git internal (config/, and likewise objects/, refs/, a file named HEAD) must remain writable when the repo root is in
allowWrite.
If HEAD/objects/refs are intended to be protected, the correctly-resolved .git/* paths should actually be covered.
Error Messages/Logs
Writing anywhere under the project's `config/` directory fails with a generic OS error that gives no hint it's git/sandbox related:
$ touch config/probe
touch: config/probe: Operation not permitted
Effective sandbox policy as reported to the session (`write.denyWithinAllow`, abridged):
/Users/me/repos/myproject/HEAD # no-op: no such path at repo root
/Users/me/repos/myproject/objects # no-op
/Users/me/repos/myproject/refs # no-op
/Users/me/repos/myproject/config # collides with the project's Rails config/ dir
Steps to Reproduce
- macOS (Apple Silicon), Claude Code
2.1.168(native binary install), sandbox enabled. sandbox.filesystem.allowWriteincludes the repo root ("./").- Use a normal git repo —
.gitis a directory, not a worktree file pointer. Confirm:
````
git rev-parse --git-dir # => .git
git rev-parse --git-common-dir # => .git
test -d .git && echo DIRECTORY # => DIRECTORY
- The repo has a top-level
config/directory (e.g. any Rails app). - Attempt a write into it from a sandboxed Bash command:
```
touch config/probe
touch: config/probe: Operation not permitted`.
**Expected:** succeeds (repo root is writable).
**Actual:**
Diagnostic showing the misalignment (which git paths are actually protected vs. the deny list):
````
touch config/probe # BLOCKED <-- the bug: project dir, not git
touch .git/config # blocked (correct, separate built-in rule)
touch .git/hooks # blocked (correct, separate built-in rule)
touch .git/HEAD # ALLOWED <-- intended target left writable
touch .git/objects/probe # ALLOWED <-- intended target left writable
touch .git/refs/probe # ALLOWED <-- intended target left writable
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.168
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
iTerm2
Additional Information
- Version: Claude Code 2.1.168, native Mach-O arm64 binary (
~/.local/share/claude/versions/2.1.168). - Platform: macOS (Darwin 25.5.0), Apple Silicon.
- Mechanism: sandbox applied in-process via the macOS Seatbelt API — no
sandbox-execchild process and no.sbprofile on disk, so the rule isn't visible in or overridable from any settings file.
$TMPDIR for the session is /tmp/claude-<uid>.
- Confirmed not config-driven:
grepacross user, project, and managed settings finds none ofHEAD/objects/refs/config/denyWithinAllow. - Related issues:
- #29316 — same root cause (git internals resolved against the worktree root, listing
HEAD, config, hooks, objects, refs), but reported for git worktrees where.gitis a file pointer, with a
different symptom (0-byte stub files in the project root). This report is the non-worktree manifestation: .git is a directory, no stub files, and the visible symptom is a project config/ dir
becoming unwritable. (#29316 is currently closed.)
- #13195 / #58178 — the separate, correctly-resolved
.git/configprotection and theallowGitConfigoverride (confirms.git/configguarding is a distinct, working code path). - #32287 — adjacent "deny/allow path resolves against the wrong root, silently" class of bug.
- Impact: legitimate writes to a project's
config/(config generators, tooling, app code that writes underconfig/) fail with an opaqueOperation not permittedand no indication the cause is the
sandbox's git protection. Concurrently, the git internals the rule was meant to protect (HEAD/objects/refs) are left writable.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗