GitSpawn Week: Untrusted Repos Are an Intake Problem
GitSpawn turns opening a folder into code execution. A Tuesday intake checklist: clone-only policy, read .git/config first, version floors, a quarantine user.
Go deeper. Build your own.
A zip from a contractor lands on the shared drive at 9:10 a.m. Someone copies it to a desktop, opens a coding agent in the folder, and the agent does what every agent does first: it runs git status to find out where it is. Git reads the repository’s own .git/config, finds a setting that names a program, and runs the program. No prompt, no diff, no model in the loop. That is GitSpawn, and by 9:11 the attacker’s code holds whatever privileges the user had.
The fix is not a smarter agent. Manifold’s advice fits on an index card: read the config before any agent touches the folder. The operational version of that card is an intake policy. Every repository reaches an agent through one door, a fresh clone from a remote you control, and anything that arrives as files goes through quarantine first.
This is the Tuesday checklist for that door: seven steps, one afternoon, no purchase order, ending with the sentence that goes into the next contractor agreement.
The attacker’s .git/config travels inside a folder, never inside a clone.
Manifold’s GitSpawn disclosure, Sep 1, 2026: what the exploit needs
Manifold Security’s Francisco Rosales published GitSpawn on Sep 1, 2026: “Eight findings across seven agents. Four remain unpatched at publication.” An agent opening a folder runs orientation commands, and Manifold shows two from different products, git status --porcelain=2 --branch and git diff --name-only HEAD; both refresh git’s index first. core.fsmonitor is a documented performance setting whose value is a helper git runs during that refresh, read from the repository’s own .git/config. So a repository can ship [core] fsmonitor = <command>, and any command that refreshes the index runs it. Manifold adds that core.fsmonitor “is not the only setting of its kind,” which is why one of the eight findings is not a fsmonitor bug at all.
Screenshot: Manifold Security, GitSpawn post header (Sep 1, 2026), captured Sep 13, 2026.
Delivery is the paragraph to read twice. In Manifold’s words, “git never carries this. Cloning a hostile URL does nothing, and neither does fetch or pull.” The repository has to arrive as files with its .git directory already inside: a shared zip, a shared drive, a sync folder, or a USB stick. That is why this is an intake article rather than a patch-Tuesday article.
Manifold’s Sept 1 update adds: “OpenAI’s Codex and Cursor were also affected… each came back as a duplicate of a report another researcher had already filed, and both have since been patched.” The per-agent versions are in step 5, dated, because they move weekly.
Screenshot: Manifold Security, GitSpawn, “The git you didn’t run” (Sep 1, 2026), captured Sep 13, 2026.
The Hacker News covered it on Sep 2, 2026, adding where the payload fires: before the workspace-trust prompt on Claude Code and Hermes, before the user has authenticated on Qwen Code, and on the first keystroke on Grok Build. paddo.dev put the scale in one line on Sep 4: “Seven agents. No prompt, no sandbox, no model in the loop. Claude Code alone ships 77 million npm downloads a month.” The download figure is paddo.dev’s. Manifold’s mitigation, verbatim: “Inspect .git/config before you open the directory with an agent. Any setting that names a program can run it.”
Why an acting agent turns a folder into an attack surface
paddo.dev’s title says the quiet part: VS Code fixed this class in 2021. An editor that runs git on open has to treat a folder as untrusted until a person says otherwise. Agents rebuilt the surface with worse defaults, because an agent does not open one folder a day. It opens whatever the task names, runs git to orient itself, and does so before its own trust dialog (documented beside Claude Code’s permission modes), in headless runs, inside CI, and under coordinators that hand out folders by the dozen.
“Which agent is patched” has a shelf life of about a week. “Which folders may an agent open, and how did they get here” does not. That second question is intake, and it sits beside the sandbox posture in the sandbox is a suggestion and the tighter tier for unattended headless runs.
The GitSpawn intake runbook: one door for every repository
Budget an afternoon for the first pass and ten minutes per repository after. Steps 1 through 4 build the door; 5 through 7 keep the agents behind it.
Step 1: Write the clone-only rule and name what it forbids
The rule is one sentence: an agent opens a repository only as a fresh clone from a remote you control, never a folder where it landed. The table is the part people argue about.
| Arrival | Hostile .git/config and hooks? |
Verdict |
|---|---|---|
git clone from an internal remote |
No, git writes a fresh config | The door |
git clone from a git bundle file |
No; bundles carry refs and objects, not the source repository’s local config | Allowed, from quarantine |
Zip or tarball with .git inside |
Yes | Quarantine |
| Shared drive, sync folder, USB, external disk | Yes | Quarantine |
Source archive with no .git |
No | Quarantine, git init, publish |
A zip without .git is not safe, merely not GitSpawn; it still gets scanned, because .gitattributes or a stray .git pointer can still name things. And a local-path clone such as git clone /media/usb/repo runs git against the hostile directory, so nobody does that as the agent user.
Step 2: Create the quarantine folder and the user who owns it
Quarantine is a directory owned by a dedicated OS user, repo-intake, with no agent installed, no tokens, and no access to any agent workspace. Agent accounts cannot read its home; it cannot write to theirs. One identity per job is the desk-level version of treating every agent as a privileged user.
Git refuses even to parse repository config it considers owned by another user unless the path is listed under safe.directory (Git documentation). An agent account that wanders into the intake folder gets a “dubious ownership” error and nothing runs. Never silence that error with safe.directory = *; that setting opts out of the check.
# illustrative, Linux/macOS
sudo useradd --create-home repo-intake && sudo mkdir -p /srv/intake/{inbox,scanned,rejected}
sudo chown -R repo-intake:repo-intake /srv/intake && sudo chmod 700 /srv/intake
The agent only ever sees the last box, and the last box only ever contains a clone.
Step 3: Read the config before anything opens the folder
Read the config with tools that do not run git inside the repository: cat, or git config --file, which parses a file without touching an index or a hook. Do it as repo-intake, from outside the directory, and read .gitattributes and the .git/hooks listing while you are there, because hooks arrive with files. Then apply Manifold’s rule literally: any setting that names a program can run it, and a repository that arrived as files has no business naming one. The list comes from git’s own configuration reference (git-scm.com) and is not exhaustive.
Setting in .git/config |
Git runs it on | Verdict |
|---|---|---|
core.fsmonitor (a string is a command; even true has no place here) |
any index refresh | Stop |
core.hooksPath |
every hook event | Stop |
core.sshCommand, core.gitProxy, core.pager, core.editor, sequence.editor |
fetch, push, interactive commands | Stop |
diff.external, diff.<driver>.command, diff.<driver>.textconv |
diff, log -p |
Stop |
filter.<name>.clean, filter.<name>.smudge, filter.<name>.process; merge.<driver>.driver |
checkout, add, merge | Stop |
credential.helper, gpg.program, alias.<name> with a shell-escaped value, include.path |
auth, signing, alias use, every command | Stop |
“Stop” means the repository goes to rejected/, the sender gets a note, and the intake record says why; it does not mean “delete the line and carry on.” If a vendor shipped a hostile config once, you want the conversation, not the cleaned file.
#!/usr/bin/env bash
# intake-scan.sh <repo-dir> (illustrative)
set -euo pipefail
r="$1"; cfg="$r/.git/config"
[ -f "$r/.git" ] && { echo "STOP: .git is a gitdir pointer"; exit 2; }
pat='^(core\.(fsmonitor|hookspath|sshcommand|gitproxy|pager|editor)|sequence\.editor|diff\.external|diff\..*\.(command|textconv)|filter\..*\.(clean|smudge|process)|merge\..*\.driver|credential\.helper|gpg\..*program|alias\..*=\W|include(if)?\..*path)'
git config --file "$cfg" --list | tr 'A-Z' 'a-z' | grep -Eq "$pat" && { echo "STOP: program-naming setting"; exit 2; }
echo "clean: $r"
Step 4: Re-publish through your remote, then clone as the agent user
A clean scan does not make the folder safe to open; it makes it safe to republish. As repo-intake, push it to an internal remote under an intake namespace, then clone from that remote as the agent user. Git writes a fresh .git/config, hooks do not travel, and the agent’s workspace never contains anything that arrived as files.
# as repo-intake
cd /srv/intake/scanned/vendor-drop
git -c core.fsmonitor=false remote add intake git@git.internal:intake/vendor-drop.git
git -c core.fsmonitor=false push intake --all --tags
# as the agent user
git clone git@git.internal:intake/vendor-drop.git
git -C vendor-drop config --list --show-origin | grep 'file:.git/config'
The last line is the receipt: every repository-local line should be one your own git wrote, core.* basics, remote.origin.*, branch.*. If a local line names a program, the pipeline leaked. When code must travel offline, ask for a git bundle instead of a zip; the USB stick then carries a file, and the clone happens on your side.
Step 5: Pin every agent to its patched floor
The door is the control. Version floors decide what happens the day someone walks around it: a patched agent makes an accidental direct open survivable for the known finding, and an unpatched one leaves the door as the only thing between a zip and a shell.
Status as published Sep 1, 2026; vendors ship weekly, so verify before citing.
| Agent | Tested version (Manifold) | Status, Sep 1, 2026 |
|---|---|---|
| Claude Code | v2.1.196 fixed the fsmonitor path; v2.1.252 still has a second path | Partial |
| Goose | v1.44.0 (CVE-2026-72718) | Patched, floor v1.44.0 |
| Hermes | v0.21.0 (CVE-2026-71963) | Unpatched |
| Qwen Code | v0.22.3 | Unpatched |
| Grok Build | v1.0.13 | Unpatched |
| Codex | v0.131.0 | Patched, floor v0.131.0 |
| Cursor | patched (Sept 1 update) | Patched |
Write the floor into the fleet inventory with a date and diff it weekly. Run each product’s documented version command and append the results to a dated log; the illustrative shell shape is claude --version; codex --version; goose --version; qwen --version. Count installations rather than product names: a CLI may exist both natively and inside WSL, and each copy needs its own version result. Release history for the patched agents lives on github.com/anthropics/claude-code, github.com/block/goose, and github.com/openai/codex.
Step 6: Override the helper class where git cannot be argued with
Three shapes, three strengths. git config --global core.fsmonitor false sets your default and nothing more: git’s precedence runs system, then global, then the repository’s own config, then the command line, so a hostile .git/config overrides your global file every time. Keep the global line as a statement of intent. Do not file it as a control.
git -c core.fsmonitor=false status is the vendor-side shape Manifold describes: a command-line setting beats every config file, but only for the commands that carry it, which is why it is the vendor’s fix and not yours. OpenAI’s record for its Codex variant, as quoted by The Hacker News, says the helper “runs outside Codex’s command sandbox and without a user-approval prompt”; a sandbox around tool calls does not wrap the git the harness runs on its own behalf.
The operator’s shape is the environment. Git reads GIT_CONFIG_COUNT, GIT_CONFIG_KEY_n, and GIT_CONFIG_VALUE_n as runtime configuration for every git process that inherits them. Those values override config files; only an explicit -c option outranks them. Set them for the agent user and every git the agent spawns is covered.
# /etc/profile.d/agent-git.sh (illustrative)
export GIT_CONFIG_COUNT=2
export GIT_CONFIG_KEY_0=core.fsmonitor GIT_CONFIG_VALUE_0=false
export GIT_CONFIG_KEY_1=core.hooksPath GIT_CONFIG_VALUE_1=/var/empty
Two limits. The override covers the keys you enumerate, not the whole class, and Manifold’s eight findings already include one that is not fsmonitor. And a user-level variable overrides a machine-level one, so this stops a hostile repository rather than a hostile developer.
Then prove it with a canary. Build a test repository in quarantine whose config reads fsmonitor = touch /tmp/gitspawn-canary, and run git status against a copy the agent user owns, as the agent user. If the marker file appears, one launcher is not inheriting the environment (an IDE started before the profile script ran, a service unit, a container with its own env). Fix the launcher and rerun.
Step 7: Put the rule in the contractor and procurement path
The zip in the opening paragraph came from a contractor because that is where zips come from, so the rule goes where code enters the company.
- Statement of work, one sentence: “Deliverables are pushed to a repository we provision, or delivered as a
git bundle. Archives containing a.gitdirectory are not accepted and will be returned unopened.” - Vendor questionnaire, three questions: which coding agents do you run against our code, at which versions, and how do you receive our repositories.
- Onboarding:
repo-intakeis the only account with the intake share mounted; engineers get the internal remote, never the share. Sync folders are not a delivery path; a client that mirrors a folder to three machines mirrors its.gitthree times.
Keep an intake record per arrival: source and arrival path, who received it and when, scan result, rejection reason, published remote, and who approved it for agents. It is the evidence that the door existed on the day it mattered.
Where repo intake fails, and the signal for each
- The drag-and-drop bypass. Someone opens the folder where it landed. Signal: agent transcripts with a working directory under a downloads, desktop, or sync root, and “dubious ownership” errors in agent logs. Those errors are the defense firing; count them.
- The sync folder resurrects
.git. A repository cleaned on one machine reappears intact on another. Signal: a scheduledfindfor.git/configunder every sync root. - Two copies of one agent. Native patched, WSL not. Signal: the version log shows two versions for one name.
- The override skipped a launcher. Signal: the canary marker appears. Fix the launcher; the policy is fine.
safe.directory = *shows up in a global config. Signal: a diff on the agent user’s global file.
Intake is operating-layer work, not a smarter prompt
Nothing in this runbook touches a prompt, a model, or a system instruction, because nothing in the exploit did. What failed was the layer around the agent: who ran it, which build, what git could execute, where the folder came from. That layer is operating infrastructure for agents, the one that already holds your restricted-mode fleet policy and the threat model for acting agents.
It is also the layer that keeps the receipts. A transcript that records which directory each session opened, and as which user, answers “did any agent touch that zip” in minutes rather than a weekend; that is the fleet replay argument applied to a folder. Windows estates get the same runbook in Intune, AppLocker, and Group Policy terms in agent allowlists as endpoint policy.
FAQ: GitSpawn and repository intake
Does git clone protect against GitSpawn?
Yes, for this class. A clone rebuilds .git locally, writes a fresh .git/config, and does not transfer hooks, so an attacker’s core.fsmonitor line never arrives. The exposure is a repository that arrives as files with .git inside: a zip, a shared drive, a sync folder, or a USB stick.
Which git config settings can run a program?
More than core.fsmonitor. core.hooksPath, core.sshCommand, core.gitProxy, core.pager, core.editor, external diff and textconv drivers, clean and smudge filters, merge drivers, credential.helper, gpg.program, shell aliases, and include.path all name something git executes or loads. In a repository that arrived as files, any one is a stop.
Sources
- Manifold Security, GitSpawn: A Single Flaw Lets Untrusted Repos Run Code in Claude Code, Codex, Cursor, and Grok (Sep 1, 2026)
- The Hacker News, Malicious .git Configs Can Make AI Agents Run Attacker Code (Sep 2, 2026)
- paddo.dev, Opening the Folder Was the Exploit (Sep 4, 2026)
- Git, configuration and environment reference
- Anthropic, Claude Code permissions documentation
- Anthropic, claude-code releases
- Block, goose releases (v1.44.0)
- OpenAI, codex releases
