Skip to content

fix(security): refuse file-tool writes to git-managed state under .git - #88747

Closed
andrexibiza wants to merge 1 commit into
NousResearch:mainfrom
andrexibiza:fix/file-tools-git-state-guard
Closed

fix(security): refuse file-tool writes to git-managed state under .git#88747
andrexibiza wants to merge 1 commit into
NousResearch:mainfrom
andrexibiza:fix/file-tools-git-state-guard

Conversation

@andrexibiza

Copy link
Copy Markdown
Contributor

What changed and why

write_file / patch (and the other mutating file tools) could silently rewrite git-managed state inside a normal repository's .git directory — HEAD, index, refs/, objects/, logs/, packed-refs, ORIG_HEAD, FETCH_HEAD, MERGE_HEAD, CHERRY_PICK_HEAD, REBASE_HEAD, COMMIT_EDITMSG, shallow, and info/* except exclude.

A single misdirected write to .git/HEAD replaces the branch identity and turns a healthy checkout into an apparently empty one (git statusOn branch evil / No commits yet). This is the same silent-corruption shape as the #78565 worktree-pointer guard, but inside the git directory itself — which that guard cannot reach.

This PR adds a git-state classifier to the shared write-deny path (agent/file_safety.py) that refuses writes under any .git directory, while keeping the user-owned paths inside a git dir writable:

  • .git/config (repository configuration)
  • .git/hooks/* (local hook scripts)
  • .git/info/exclude (per-repo ignore rules)
  • .git/description

Everything else under .git/ is git-managed state that a misdirected write silently corrupts. The guard runs on os.path.realpath()-expanded paths, so a symlink pointing into a git dir cannot bypass it.

Reproduction (before this fix, on main)

# in any git repo
write_file('<repo>/.git/HEAD', 'ref: refs/heads/evil\n')   # OK — no error
git status  # → On branch evil / No commits yet

After this change, write_file('<repo>/.git/HEAD', ...) returns a write-denied error, and the repo's git state is untouched.

How to test

scripts/run_tests.sh tests/agent/test_file_safety_git_state.py   # 28 new regression tests
scripts/run_tests.sh tests/agent/test_file_safety.py \
  tests/agent/test_file_safety_credentials.py \
  tests/agent/test_file_safety_session_state.py                   # existing guards still green

The new suite asserts:

  • git-managed control paths are denied: .git/HEAD, .git/index, .git/packed-refs, .git/ORIG_HEAD, .git/FETCH_HEAD, .git/MERGE_HEAD, .git/CHERRY_PICK_HEAD, .git/REBASE_HEAD, .git/COMMIT_EDITMSG, .git/shallow, .git/refs/*, .git/objects/*, .git/logs/*, .git/info/refs, .git/info/alternates, and the .git dir itself.
  • user-owned paths stay writable: .git/config, .git/description, .git/hooks/*, .git/info/exclude.
  • normal non-git files and unrelated directories are untouched.
  • the denial surfaces a clear Write denied: ... protected system/credential file error.

Platforms tested

  • Windows 11 (git-bash), Python 3.11 — full new suite green (28 passed), existing file-safety suites green.
  • git diff --check clean; scripts/check-windows-footguns.py scans clean.
  • Note: tests/agent/test_file_safety_sandbox_mirror.py has 4 pre-existing failures that also occur on pristine origin/main (an environment/cross-profile artifact unrelated to this change — verified on a clean main worktree).

Why this matters to users

Today, one misdirected write_file call — or a prompt-injected path — can replace the contents of a repository's .git/HEAD, changing which branch the checkout reports and making a healthy repository look empty or corrupted. The working tree itself is untouched, so it appears as silent state corruption that's easy to miss until git status goes wrong.

After this change, the file tools refuse to write git's own control files. Your repositories' git state (branches, refs, index, objects, logs) can no longer be silently clobbered by a stray file-write, while the git files you legitimately own and edit (config, hooks, info/exclude) stay fully writable. Nothing about normal file editing changes — only the dangerous, git-managed control surface is now protected.

Fixes #78793

write_file/patch (and the other mutating file tools) could silently
rewrite git's own control files inside a normal repository's .git
directory — HEAD, index, refs/, objects/, logs/, packed-refs,
ORIG_HEAD, FETCH_HEAD, MERGE_HEAD, CHERRY_PICK_HEAD, REBASE_HEAD,
COMMIT_EDITMSG, shallow, and info/* except exclude. A single
misdirected write to .git/HEAD replaces the branch identity and turns
a healthy checkout into an apparently empty one (same silent-corruption
shape as the NousResearch#78565 worktree-pointer guard, but inside the git dir).

Add a git-state classifier to the shared write-deny path that refuses
writes under any .git directory while keeping user-owned paths writable:
.git/config, .git/hooks/*, .git/info/exclude, .git/description.

Fixes NousResearch#78793
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint tool/file File tools (read, write, patch, search) sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data duplicate This issue or pull request already exists labels Aug 17, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Duplicate of #78806 — both block generic file-tool writes to git-managed .git state; #78806 is the broader canonical implementation with dedicated denial handling.

@andrexibiza

Copy link
Copy Markdown
Contributor Author

Closing into #78806

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint duplicate This issue or pull request already exists P2 Medium — degraded but workaround exists sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data tool/file File tools (read, write, patch, search) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: write_file/patch silently corrupt git-managed state in normal repos (.git/HEAD, refs/, index)

2 participants