Back to all posts
Published on · by Renaud Deraison

git status ran their code

On September 2, 2026, Manifold Security disclosed GitSpawn: eight flaws across seven command-line coding agents where a repository's own .git/config runs a command on the developer's machine. The agent runs git status to see what branch it is on, git refreshes its index, and the repository's chosen command executes with the logged-in user's privileges, outside the sandbox, ahead of the trust prompt, and on one tool ahead of the login. No prompt, no tool call, no model turn. Bromure Agentic Coding does not patch that path. It moves the machine the path leads to.

You typed nothing and approved nothing. The agent had not finished starting, and on one of these tools you had not even logged in. A folder somebody sent you ran a command as you, and git is what ran it.

A contractor finishes a piece of work and sends you a zip of the project directory in a shared folder. You unzip it, cd in, and start your coding agent to get oriented. The agent begins by asking git what branch this is and what is dirty. That is the whole exploit.

On September 2, 2026, The Hacker News reported a disclosure by Manifold Security covering eight flaws across seven command-line coding agents, published under the name GitSpawn. Between them the affected tools carry close to half a million GitHub stars, and Claude Code alone is past 77 million npm downloads a month. Four of the eight were still executing repository-supplied commands when Manifold retested them on September 1.

A performance setting whose value is a command

core.fsmonitor is a git option for people with enormous repositories. Rather than walk the whole tree to find out what changed, git can call an external helper program and ask it. The value of the setting is the command. Git reads it from the repository's own .git/config and runs it whenever it refreshes its index, which means on git status, on git diff, on the plumbing every tool leans on.

Nearly every agent Manifold examined gathers project context by running those commands at startup, and none of them looked at .git/config first. So the repository names the command and git runs it. In the researchers' words, it executes "with the full privileges of the logged-in user, outside the sandbox." OpenAI, in its own advisory for the Codex variant, said the helper "runs outside Codex's command sandbox and without a user-approval prompt."

A git clone does not carry any of this. Clone transfers objects and refs, and the source repository's local config stays home. So the poisoned project has to reach you as files, with its .git directory intact: a zip archive, a shared drive, a synced folder, a USB stick. Manifold's list describes how consultants, clients, and colleagues hand off work, and it is the one delivery path with no pull request in it. Git's own guard against hostile repositories, safe.directory, checks whether somebody else owns the repository. You unzipped it, so you own it, and the guard has nothing to say.

The timing puts each agent's own protection one step too late. On Claude Code and Hermes Agent the payload fires before the workspace-trust prompt, the dialog that exists to ask whether you trust this directory. On Qwen Code it fires before the user has authenticated, and on Grok Build it fires on the first keystroke.

GitSpawn — from the handoff to the shellthe folder arriveszip, shared drive,synced folder, USB —.git/config intactthe agent startsgathers context:git status · git diffgit refreshes its indexreads the repo's owncore.fsmonitor = …<their command>your user, your env,your home directoryWhat had not happened yetthe trust promptClaude Code,Hermes Agentsigning inQwen Code fires beforeauthenticationtyping anythingGrok Build fires onthe first keystrokethe modelno tool call, no turn,nothing to logEvery gate in the row above is a step in the agent's startup sequence. The command ran before the sequence reached them.
The whole chain, and the four things that had not happened yet when it finished. A repository's .git/config names a command in core.fsmonitor; the agent runs git status to work out its branch; git refreshes the index and calls the helper. It executes as the logged-in user, ahead of the workspace-trust prompt on Claude Code and Hermes Agent, ahead of authentication on Qwen Code, and on the first keystroke on Grok Build, with no tool call and no model turn anywhere in the sequence.

Eight findings, seven agents, four of them still open

Half of these are fixed. goose 1.44.0 carries the fix for CVE-2026-72718, rated CVSS 7.0. OpenAI shipped Codex CLI 0.131.0 and the 26.519.x desktop builds for CVE-2026-19592. Cursor patched its variant. Manifold confirmed Claude Code's core.fsmonitor path in 2.1.193 and Anthropic closed it in 2.1.196, without publishing an advisory: Manifold notes that neither Claude Code finding appears in npm's advisory records.

The other half has not moved. Hermes Agent has a CVE (CVE-2026-71963) and a private advisory still untriaged after six contact attempts. The Qwen Code maintainers accepted the report on July 7 and 0.22.3 still runs the command. Grok Build still runs it. And Claude Code has a second path, through its ultrareview command, which abuses a different git configuration key and was still live at 2.1.252. Manifold withheld that key's name rather than hand out a reusable attacker template.

Anthropic patched the first path in three days. The class walked around it through a different key, in the same file, in the same product.

This is git's execution surface, inherited by every tool that shells out to git, and git has more than one setting whose value is a command it will run for you. Pagers, editors, SSH commands, hook paths, diff and filter drivers, and aliases beginning with a shell escape all take command values. Every vendor now has to work out, key by key, which of those a repository can reach from its startup path, against a configuration schema that gains keys over time. Manifold's recommended fix, sanitizing the git configuration during background context operations and disabling core.fsmonitor, is a denylist maintained against a namespace. The researchers found the pattern in more agents than the report names.

The controls that would have caught this run downstream of it

Line up what a careful team has on a coding agent today: permission prompts on tool use, workspace trust, hooks that inspect a command before it runs, the transcript endpoints that ship a session to a compliance backend, and OpenTelemetry for permission decisions and bypasses.

All of them anchor to the agent's own loop, meaning a tool call the agent decided to make or a turn the model took. GitSpawn produces neither. The process that executes the payload is git, invoked by the agent for its own housekeeping, and the command comes from a file the agent never read. There is no tool call to approve and no hook to fire. A team watching agent transcripts land in its SIEM sees a session that starts normally and shows nothing, the same blind spot we wrote about when Anthropic shipped its Compliance API: a log anchored to what reached the model cannot see what ran before the model was consulted.

The command is going to run. The question worth answering is what it finds.

Unzipped into your home directorygit status → core.fsmonitor helperchosen by the folder, run as youwhat is one directory away~/.ssh/id_ed25519ANTHROPIC_API_KEY, AWS_SECRET_ACCESS_KEY~/.git-credentials, ~/.aws, ~/.kube/config~/.zshrc — persistence for next loginoutbound: a normal connection from anormal developer tool, on a normal portthe agent's own log has no row for any of itThe folder was untrusted. The account it landed in was not.Opened in a Bromure workspacegit status → core.fsmonitor helperstill runs — as ubuntu, in the guestdisposable Linux VM · booted before the folderno private key bytes — host ssh-agent signs over vsockenv and config credentials are decoys (brm_…):80 and :443 diverted into the host proxy, unsettableegress firewall: default deny → no second stagea decoy leaving its scope → 451, VM paused,workspace flagged, wipe required to boot againevery request gets a row, whatever process made itThe command still runs. It just runs somewhere that was built for it.
The same folder, the same git status, two different machines. On your laptop the helper runs as you, one directory from your SSH keys and your cloud credentials, and its outbound connection is an ordinary connection from an ordinary developer tool. In a Bromure workspace it runs as ubuntu inside a VM that booted before the folder existed: no private key bytes to steal, decoys where the credentials should be, and every byte of egress on a wire the host owns and records.

Run it and see what it finds

Bromure Agentic Coding gives each workspace its own Ubuntu VM under Apple's hypervisor. The agent, its git, its package manager, and anything those three start all live in the guest. Drop the contractor's zip into a workspace, let the helper fire, and grant the attacker everything: pre-trust, pre-auth, root in the guest if you like.

Read the SSH keys. There are none to read. A workspace's private keys stay on your Mac; the VM gets an SSH_AUTH_SOCK bridged over vsock to a host-side agent. That protocol has a request meaning "sign this challenge" and no request meaning "hand me the key."

Sweep the environment for credentials. They are all there, shaped the way validators expect, and all of them are decoys. ANTHROPIC_API_KEY holds an sk-ant-api03-brm-… fake, and so do ~/.git-credentials, ~/.aws/config, ~/.kube/config, and ~/.docker/config.json. Your real values live encrypted on your Mac, and the proxy substitutes them onto the wire after the request has left the VM, only when it is bound for the host that credential was minted for. No switch turns this off. It is how credentials work in the app.

Pull down the second stage. The helper is a loader, a few hundred bytes whose job is to fetch the real payload. That fetch runs into the workspace's egress firewall. Set Unmatched traffic to Deny and the VM reaches the hosts you listed and nothing else, enforced in two places: the virtual switch matches by destination IP and DNS-snooped hostname across all protocols, and the proxy matches again by TLS server name and, for web rules, by HTTP method. A loader that reaches for a package registry instead meets the supply-chain pipeline, where the age gate refuses versions younger than the cutoff. A payload published yesterday cannot clear that bar.

Exfiltrate what it did find. Now something has to cross a wire your Mac owns. The proxy scans each outbound request against the workspace's minted decoys, headers and body, with an Aho-Corasick automaton. A decoy addressed to a host outside the scope it was minted for is the signature of a machine sending out something it should not know. The proxy blocks with HTTP 451 before a byte reaches the destination, pauses the VM, and raises an alert naming the credential and both hosts. Bromure then marks the workspace compromised, and it will not boot again until you wipe its disk and home.

Leave persistence. A modified .bashrc in the guest lasts as long as you let the VM last. Erase home resets /home/ubuntu, Reset disk re-clones the system disk from the signed base image, and bromure-cli vm run --rm gives you a workspace that deletes itself when the VM stops.

The interception has no environment variable to unset

A proxy the guest opts into is a proxy the guest can decline. Bromure's virtual switch diverts the VM's port-80 and port-443 flows into the host proxy itself: no HTTPS_PROXY to unset, nothing in the guest that turns it off, and plain HTTP inspected the way HTTPS is. The process making the request here never read the agent's configuration and is not the agent. It is a helper binary git launched, and it rides the same wire as everything else.

The row exists even though no tool call does

The host proxy writes a metadata record for each request out of the VM, whichever process made it: time, host, port, method, path, status, latency, and bytes measured before the credential swap. Firewall verdicts, supply-chain decisions, and credential brokering land in the Security Timeline (Window → Security Timeline…), and on an enrolled Mac they go to the organization's event stream as egress.firewall, supply_chain.fetch, and credential.exfiltration. An agent-side transcript of this attack is blank while the wire's carries the request.

The guest can reach one thing on your Mac. A shared folder is a live, read-write window into your real filesystem, and the compromise wipe leaves it alone by design. Share the project directory rather than your home directory, and the helper's find for *.pem walks a Linux home full of checkouts and package caches instead of your keychain-backed logins.

The boundary that is not a step in the sequence

Read the four GitSpawn timing details together, ahead of the trust prompt, ahead of authentication, ahead of the first keystroke, ahead of any model turn, and they say one thing. Each protection is a step in a startup sequence, and each new feature adds a step ahead of it. Cursor learned that in 2025 with a repository-supplied mcp.json, and again this July with a worktree setup command that ran before its own trust prompt. Now it is git's turn, in seven products at once, through a config key that has been in git for years.

A hypervisor is not a step in that sequence. It does not read .git/config. No refactor reorders it and no code path exempts itself from it, because it does not run in the same program as the prompt. The VM was running before the zip finished downloading, and its two guarantees, no real secrets in here and no unmediated route out, hold the same at 2.1.193 and at 2.1.196.

Check your .git/config before you open a folder somebody sent you. Manifold is right to say so. You will also do it for two weeks and then skip it on the third zip of a busy afternoon, which is the shelf life of most instructions that end "before you open it." Install Bromure Agentic Coding, give the unfamiliar folder its own machine, and let the config run whatever it likes in a room where running is all it gets to do.