The Exploit Ships in .git/config

The Exploit Ships in .git/config

I reproduced the core of this attack in about thirty seconds, and the scary part is how boring every step looked:

$ git init demo && cd demo
$ git config core.fsmonitor "sh -c 'echo GITSPAWN-PAYLOAD-RAN >> /tmp/gspawn-proof.txt'"
$ git status --porcelain=2 --branch
$ cat /tmp/gspawn-proof.txt
GITSPAWN-PAYLOAD-RAN
GITSPAWN-PAYLOAD-RAN

No exploit framework. No CVE-worthy git bug. Git did exactly what its documentation says it does: the repository’s .git/config set core.fsmonitor to a command, git status refreshed the index, and git ran the command. Twice, because git probes the fsmonitor helper at start and stop.

That exact git status --porcelain=2 --branch line is not a random choice. It is the command Hermes Agent runs when you open a repository and send your first message, gathering the workspace snapshot for its system prompt. On September 1, Manifold Security published GitSpawn: eight findings across seven AI coding agents, all sharing one property. The agent runs git in the background to learn where it is. The repository decides what that git invocation executes. Four of the eight findings were still unpatched at publication.

The mechanism: a config file that names a program

core.fsmonitor is a performance feature for large repositories. Instead of stat-ing every file, git asks a helper program what changed, and it runs that helper during any index refresh. Almost every git command that touches the working tree refreshes the index first. git status does. git diff does.

Git reads that setting from the repository’s own .git/config. So the config above is a complete weapon. Any git command that refreshes the index inside that directory executes the attacker’s line as you, with your environment, on your machine.

Delivery is the constraint that keeps this from being remotely exploitable, and it is worth being precise about, because instinct gets it backwards. Cloning a hostile URL transmits nothing dangerous. Clone, fetch, and pull never copy a hostile .git/config. The repo has to arrive as files with its .git directory already inside: a zip, a shared drive folder, a sync client, a USB stick. The way colleagues and consultants hand projects to each other every day. Every proof of concept in Manifold’s writeup used a zip.

From there the failure is architectural, and it repeats across every affected agent. The context-gathering git call is the agent’s own subprocess, spawned by the agent’s own code, before any model call, before any tool runs. It sits outside the sandbox the agent applies to model-driven actions and outside the approval model entirely. The permission gate you configured never saw the command, because as far as the agent’s security model is concerned, no command happened. A context probe isn’t a tool call.

Eight findings, seven agents, four still open

Manifold’s table, condensed. Every row was reported privately; each unpatched finding was re-confirmed against a current release on September 1 before publication.

AgentReportedStatus at publication
Claude Code (core.fsmonitor)Jun 26, on 2.1.193Patched in 2.1.196 (closed as a duplicate of a same-day report)
OpenAI CodexJul 20Patched (duplicate of an earlier report; mechanism differs slightly, same class)
CursorJul 8Patched (duplicate)
GooseJul 13, on 1.41.0Patched in 1.44.0 — CVE-2026-72718, CVSS 7.0
Qwen CodeJul 7, on 0.19.6, accepted by Alibaba’s SRCUnpatched — confirmed on 0.22.3 Sep 1; executes at startup, before authentication
Grok BuildJul 14, on 0.2.93Unpatched — confirmed on 1.0.13 Sep 1; executes on the first keystroke
Claude Code (ultrareview)Jul 15, on 2.1.210Unpatched — confirmed on 2.1.252 Sep 1; a different config key of the same kind, name withheld
Hermes AgentJul 20, on 0.18.2Unpatched — confirmed on 0.21.0 Sep 1; CVE-2026-71963, assigned by VulnCheck

The Goose advisory is the cleanest public record of the class. goose review built its diff with git_command() passing only -c core.quotePath=off, never stripping core.fsmonitor; the resulting git diff HEAD refreshed the index and ran the repo’s command “before goose ever contacted the model,” with no tool approval and no trust prompt. Affected versions: anything below 1.44.0. The impact line is the honest one: the command runs unsandboxed and inherits the user’s environment, so it reaches SSH keys, provider API keys, and everything in your shell config.

The Hermes row has an extra uncomfortable layer. Manifold describes six contact attempts across five channels, a private GitHub advisory that was never triaged, and a CVE number assigned not by the vendor but by VulnCheck, an independent CNA, after the vendor went quiet. Whatever the internal reasons, the observable outcome is that the disclosure process for this agent currently runs through third parties.

What I could verify from here

Everything above is Manifold’s reporting. I wanted to check the claims that touch this machine, so three things, each checkable:

The git mechanism is real and current. The transcript at the top of this post ran on git 2.53.0 on Linux, today. core.fsmonitor behaving this way is documented, intended behavior — git-config(1) describes exactly this execution — which is why nobody calls it a git bug. The vulnerability lives in what the agents do with it.

The probe command matches. The installed Hermes Agent on this box is v0.20.6 (upstream 561b053f). Its workspace snapshot runs git -C <root> status --porcelain=2 --branch (in agent/coding_context.py, via the shared bounded_git_probe wrapper). The wrapper solves a real problem — unbounded Windows pipe drains after timeout kills — and sanitizes nothing about git config. No -c core.fsmonitor=false, no GIT_CONFIG_GLOBAL/GIT_CONFIG_SYSTEM overrides on this path. Manifold re-confirmed the same class on 0.21.0 on September 1; nothing in the local history through August 31 touches it.

The codebase already knows the pattern. tools/checkpoint_manager.py sets GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM to /dev/null for the git repos Hermes manages itself. The isolation idiom exists in the tree. It just isn’t applied to the startup probe, which is the one subprocess an untrusted directory can influence before any trust decision is made. That gap between “we know how to neutralize repo config” and “we did it here” is the whole story of this class.

One more check I did not do: I never ran a booby-trapped repo against any agent. The mechanism is verified, the probe command is verified against installed source, and the end-to-end exploit is Manifold’s demonstration, not mine.

The duplicates are the tell

Five of the eight reports came back as duplicates of findings other researchers had already filed — one on the same day. Codex and Cursor, added in Manifold’s September 1 update, were both duplicates too. Read that as a measurement: multiple independent researchers, hitting the same defect from different directions within weeks of each other. When that happens, the bug is not a mistake one team made. It is what falls out of the architecture everyone shares.

It falls out because every CLI agent converges on the same startup flow: resolve the working directory, detect the project, gather git context, build the system prompt. The context-gathering step happens before the model is involved, so it gets none of the machinery the model’s actions get. No sandbox. No approval. No trust prompt on some agents. The industry spent two years building permission systems for what the model asks to do and left the agent’s own subprocesses outside the walls.

Vendor response has been equally instructive, in both directions. Anthropic and OpenAI patched fast, but both initially closed reports as duplicates — of reports they were also sitting on. xAI closed an earlier report of the same class as “informative.” Alibaba accepted the Qwen Code report and then shipped two minor versions without a fix. Goose is the counterexample: acknowledged, CVE’d, fixed, advisory published with root cause and CVSS. The difference between those outcomes is not security resources. It is whether the vendor treats “the agent runs a subprocess before the trust prompt” as attack surface at all.

What to actually do

If you receive repositories as files — zips, shared drives, client hand-offs — inspect the git config before any agent touches the directory. The one-liner:

git -C /path/to/repo config --list --show-origin | grep -Ei 'fsmonitor|filter|ssh|command|hooks'

Any setting that names a program is executable in the right context. core.fsmonitor is the known sink; Claude Code’s second, still-unpatched finding turns on a different config key of the same kind, which Manifold deliberately left unnamed. “I only check fsmonitor” is a blocklist, and blocklists lose to enumeration.

If you ship an agent, the fix is one flag on the probe path, as Manifold puts it:

git -c core.fsmonitor=false status

Better, strip the whole class rather than the known key: run startup probes with GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM pointed at /dev/null (the pattern already sitting in Hermes’ own checkpoint manager), or add -c core.fsMonitor=false -c core.pager=cat and audit for remaining execution sinks. And treat the probe like what it is — a subprocess triggered by untrusted input — which means sandboxing it or at minimum routing it through the same visibility as model-driven commands.

If you run agents on machines that hold credentials, assume that opening an untrusted folder with an agent is equivalent to running code from it, because on at least five current releases it literally is. Until the unpatched four ship fixes, the zip from a client deserves the same suspicion as an executable attachment.

The boundary was never where the UI drew it

Every agent in this story draws a bright line somewhere: a workspace-trust dialog, an approval prompt, a sandbox around model actions, an “auto-approve except these tools” policy. GitSpawn doesn’t cross any of those lines. It executes before they exist, through a subprocess the agent’s own startup code spawns for a purpose as innocuous as putting the branch name in a prompt.

That is the uncomfortable generalization. The skills, MCP servers, and plugins an agent loads arrive as files with their own configuration too, and Manifold closes on exactly that parallel. The artifact changes; the trust model doesn’t. An agent is a program that grants executable authority to whatever its working directory contains, and until the first subprocess it runs is treated as the first trust decision, the perimeter is drawn one step too late.


Sources

Keep reading