On 1 September 2026, Manifold Security published GitSpawn: seven coding agents, including Claude Code, Codex, Cursor, Goose, Hermes Agent, Qwen Code and Grok Build, ran routine git commands for context without stripping the repository's own git configuration, so a folder could make git run a program of its choosing before any approval prompt. On 17 September, Air Security published Plugin4Shell: Claude Code, Codex, GitHub Copilot and Gemini CLI installed plugins at a pinned commit without checking that they actually got that commit. Both bugs come down to one fact that predates AI agents: opening a folder with git in it can run code. Update your agents, read .git/config before anything runs git in a folder you did not clone yourself, and open untrusted code in a disposable environment with no credentials.
Key takeaways
- Git honors settings in a repository's own
.git/configthat name programs to run.git statusandgit diff, the commands agents run for context, trigger some of them. - A normal
git clonenever copies.git/config. The vector is a project that arrives as files with.gitinside: a zip, a shared drive, a sync folder. safe.directorydoes not help when you own the folder, which you do after unzipping it.- Several agents fixed GitSpawn by 17 September: Claude Code, Goose, Hermes Agent, Qwen Code, Codex and Cursor. Manifold found no fix for Grok Build or for a separate Claude Code
ultrareviewfinding. - A pinned commit is only a pin if you verify
HEADafter checkout. Plugin4Shell turned unverified pins into code swaps. - Treat incoming project folders like untrusted executables: inspect first, then open in a VM or container without credentials.
What GitSpawn showed, from the disclosure
Coding agents gather context before you ask them anything. They run git status to see what changed and git diff to see how. Manifold's finding is that these calls "ran without stripping the repository's own git configuration, and several git settings are command execution sinks."
The main sink is core.fsmonitor, a performance setting. When it holds a path, git runs that program to ask which files changed, and it does so whenever it refreshes the index, which git status and git diff both do. The repository's own .git/config is a valid place to set it.
Manifold is precise about delivery, and it matters for your threat model: "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, so the vector is anything that moves a directory instead of cloning it: a shared .zip, a shared drive, a sync folder, a USB stick." Contractors hand projects over this way. So do clients, and so do candidates sending take-home tests.
When the code ran depended on the agent. Claude Code ran git status at startup, before the workspace-trust prompt was accepted. Qwen Code ran it on start-up, before the user had authenticated. Grok Build ran it on the first keystroke of a prompt. Goose ran it while gathering the diff for goose review.
| Agent | Status in Manifold's post (1 September) | Vendor fix by 17 September |
|---|---|---|
Claude Code, startup git status | Fixed in 2.1.196 | 2.1.196 (29 June). The 2.1.265 changelog (8 September) also stops its git probes running clean filters from nested repositories |
Claude Code, ultrareview | Unpatched on 2.1.252; uses a different git setting, which Manifold left unnamed | No public fix found |
| Goose | Fixed in 1.44.0, CVE-2026-72718 | 1.44.0 (23 July) |
| Hermes Agent | Unpatched on 0.21.0, CVE-2026-71963 | Fix merged 2 September, shipped in 0.21.1 (7 September) |
| Qwen Code | Unpatched on 0.22.3 | 0.23.4 (14 September) |
| Grok Build | Unpatched on 1.0.13 | No public fix found |
| OpenAI Codex | Patched, reported as a duplicate | Version not stated |
| Cursor | Patched, reported as a duplicate | Version not stated |
None of this is new in kind. SonarSource disclosed the same core.fsmonitor path in Claude Code in April, fixed in late 2025, alongside commands in .claude/settings.json that ran before the trust dialog. The class keeps coming back because every new code path that shells out to git can reintroduce it.
Plugin4Shell: when a pin is not a pin
Plugin marketplaces in coding agents pin each plugin to a commit hash, so the code you reviewed is the code you run. Air found that the affected agents cloned the plugin repository, checked out the pinned hash, and never confirmed the result. In Air's words, "Every affected agent checks out the pinned commit but never checks that it actually landed there."
The trick is a git naming rule. When the argument to git checkout matches a local branch name, git switches to that branch, even if the same string is also a full commit hash, and only prints a "refname is ambiguous" warning. We reproduced this in a scratch repository: git checkout <hash> and even git checkout --detach <hash> landed on the branch's code, while git rev-parse <hash> still returned the real commit. After a clone, the remote's default branch exists as a local branch. So an attacker who controls the plugin repository creates a branch named exactly like the pinned hash, makes it the default, and the pin silently resolves to their code. Gemini CLI fell to a variant: a default branch named FETCH_HEAD.
Two conditions limit it. The branch must be the default, because a non-default branch arrives only as a remote-tracking ref and the checkout falls back to the commit. And the host must allow a 40-character hex branch name: GitHub rejects one outright, while Bitbucket and self-hosted git servers allow it. Auto-update makes it zero-click: the same checkout runs in the background when the marketplace bumps the pin.
| Agent | Status per Air Security (17 September) |
|---|---|
| Claude Code | Fixed in 2.1.179 (published 16 June) |
| OpenAI Codex | Fixed in 0.146.0; OpenAI's release (29 July) includes "Verify Git plugin SHA checkouts" |
| GitHub Copilot | No fix shipped |
| Gemini CLI | No fix planned; Google is moving users to Antigravity CLI and stopped serving individual users on 18 June |
Why git's own protections do not cover this
Git has defended against hostile repositories for years. Those defenses were built for a different situation.
safe.directory checks ownership. It arrived in Git 2.35.2 in April 2022, for CVE-2022-24765, where another user on a shared machine could plant a repository in a directory you would walk into. Git now refuses to read the config of a repository owned by someone else. When you unzip a project into your home directory, you own it, so git reads its config.
Only a few settings are protected. The git documentation calls system, global and command-line config "protected configuration". A short list of settings, among them safe.directory, safe.bareRepository and uploadpack.packObjectsHook, is ignored anywhere else. Everything below is honored from the repository's own .git/config:
| Setting | What git runs | Triggered by |
|---|---|---|
core.fsmonitor (a path) | The fsmonitor hook | Index refresh: git status, git diff and others |
filter.<driver>.clean / .process, wired up by .gitattributes | The filter program | git add, and git status when a file's stat data has changed |
diff.external, diff.<driver>.textconv / .command | External diff or text conversion | git diff, git log -p, git show |
core.hooksPath | Hooks from a directory the repository chooses | Commit, checkout, merge, push |
core.sshCommand | A replacement for ssh | Fetch and push over ssh |
credential.helper | A helper, or any shell command if it starts with ! | Anything that needs credentials |
gpg.program | A replacement for gpg | Signing, and verifying signatures in git log |
core.pager, pager.<cmd>, core.editor, sequence.editor | Pager or editor | Interactive use with a terminal |
alias.<name> starting with ! | A shell command | Running that alias |
We checked the first three rows on a scratch repository with a harmless marker script. git status ran the fsmonitor program and a clean filter. git diff ran diff.external. git rev-parse, git log and git branch --show-current ran nothing.
Cloning is safe; moving folders is not. This is why most people never hit it. Repositories you clone from GitHub get a fresh .git/config written by your own git. The risk sits entirely in the folders that skip that step.
The pre-open check (script)
Before you, your editor or an agent runs git in a folder that arrived as files, read it without running git inside it. This script does that. It reads the git config with git config --file, which treats the config as a plain file, and it never runs git inside the target folder.
#!/usr/bin/env bash
# preopen.sh: inspect a project folder before git, an editor or an agent touches it.
# Reads files only. Never runs git inside the target folder.
set -uo pipefail
repo="$(cd "${1:?usage: preopen.sh <folder>}" && pwd)"
cd / # keep git's repository discovery away from the target
findings=""
add() { findings+="$1"$'\n'; }
gitdir="$repo/.git"
if [ -f "$gitdir" ]; then # a .git *file* can point anywhere on disk
target="$(sed -n 's/^gitdir: //p' "$gitdir")"
add "git: .git is a file pointing to $target"
case "$target" in /*) gitdir="$target" ;; *) gitdir="$repo/$target" ;; esac
fi
# Settings that make git start a program. Includes are reported, not followed.
risky='^(core\.(fsmonitor|hookspath|sshcommand|pager|editor|askpass|gitproxy)|sequence\.editor'
risky+='|diff\.external|diff\..+\.(textconv|command)|merge\..+\.driver'
risky+='|filter\..+\.(clean|smudge|process)|credential\.(.+\.)?helper|gpg\.(.+\.)?program'
risky+='|pager\..+|alias\..+=!|include\.path|includeif\..+\.path)'
if [ -f "$gitdir/config" ]; then
while IFS= read -r line; do
case "$line" in core.fsmonitor=true|core.fsmonitor=false) continue ;; esac
add "git config: $line"
done < <(git config --file "$gitdir/config" --no-includes --list 2>/dev/null | grep -iE "$risky")
fi
while IFS= read -r h; do add "git hook: $h"; done < <(
find "$gitdir/hooks" -type f ! -name '*.sample' 2>/dev/null)
# Repositories hidden inside the tree: nested .git entries and bare repositories.
while IFS= read -r g; do add "git: nested repository at $g"; done < <(
find "$repo" -path "$gitdir" -prune -o -name .git -print 2>/dev/null)
while IFS= read -r h; do
d="$(dirname "$h")"
[ -d "$d/objects" ] && [ -d "$d/refs" ] && add "git: embedded bare repository at $d"
done < <(find "$repo" -path "$gitdir" -prune -o -type f -name HEAD -print 2>/dev/null)
# Files that steer agents, editors or containers.
for f in CLAUDE.md AGENTS.md GEMINI.md .cursorrules .mcp.json .claude/settings.json \
.github/copilot-instructions.md .vscode/tasks.json .devcontainer/devcontainer.json; do
[ -e "$repo/$f" ] && add "review: $f"
done
grep -qs '"folderOpen"' "$repo/.vscode/tasks.json" && add "editor: a VS Code task runs on folder open"
grep -qs '"initializeCommand"' "$repo/.devcontainer/devcontainer.json" &&
add "container: devcontainer initializeCommand runs on the host"
if [ -z "$findings" ]; then echo "clean: no execution settings found"
else printf '%s' "$findings"; echo "Inspect every line above before running git, an editor or an agent here."; exit 1
fiWe tested it against a folder seeded with a hostile core.fsmonitor, a ! alias, a filter driver, an include, a hook, a nested repository, a bare repository and a folder-open task. It reported all of them, and the marker hook never fired. A clean git init folder exits 0.
Any git config line in the output is a stop sign until a person has read it. The review: lines are not alarms by themselves: instruction files such as CLAUDE.md and AGENTS.md are normal, but they steer an agent, so read them like code. Our post on agent-assisted attacks shows what a hostile instruction file did in a real intrusion.
Opening untrusted code safely: VM, no credentials, no network
If the check finds anything, or the folder comes from someone you do not know, open it somewhere it cannot hurt you. A throwaway container with no network and no home directory is enough to look around with git:
docker run --rm -it --network none \
-v "$PWD/client-project:/work" -w /work \
--entrypoint sh alpine/gitInside, git can run whatever the config names, and it reaches nothing: no SSH keys, no cloud credentials, no network. To work on the project with an agent, use a VM or dev container that holds only the project and the agent's model API key, with egress limited to the model provider, and no SSH agent, cloud profile or package-registry token forwarded in. Remove the hostile settings, or re-create the repository by cloning from its real remote, before the project ever touches your host.
The same thinking applies to CI. A job that checks out a pull request and runs an agent with repository secrets is opening untrusted code with credentials attached. Our analysis of the TanStack and AntV attacks covers why the runner is the target.
For tool builders: sanitize git config and verify pins
If your product shells out to git, the fix Manifold recommends is to override the dangerous settings on every background call. Command-line -c has the highest precedence, above the repository's config and above GIT_CONFIG_* environment pairs:
# background context probes: neutralize settings that start programs
git -c core.fsmonitor=false -c core.hooksPath=/dev/null status --porcelain=v2 --branch
git -c core.fsmonitor=false diff --no-ext-diff --no-textconv --name-only HEADFor child processes you do not control, the environment form does the same for one key:
GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.fsmonitor GIT_CONFIG_VALUE_0=false git statusKnow the limits. This is a denylist, and git has more programs than one flag covers: clean filters declared through .gitattributes still ran on git status in our test. Refuse to run background git in a repository whose local config defines filter or diff drivers until a person has looked, and run your git probes inside the same sandbox as the agent's other commands, not as a host subprocess. Manifold notes that Claude Code's startup git status ran outside its sandbox.
For pins, do what Air recommends and check the commit you actually got. Force git to read the hash as a commit, then verify HEAD:
PIN=0123456789abcdef0123456789abcdef01234567 # the commit the marketplace pinned
git clone --quiet --no-checkout "$PLUGIN_REPO" plugin && cd plugin
git checkout --quiet --detach "$PIN^{commit}" # ^{commit} resolves the object, not a branch named like it
test "$(git rev-parse HEAD)" = "$PIN" || { echo "pin mismatch: refusing plugin" >&2; exit 1; }Run the same check on every auto-update, not only on first install. For the broader dependency side, our software supply chain security guide covers pinning and provenance beyond git.
Where teams get caught
- The take-home and the handover zip. Candidates, contractors and clients send folders, not clone URLs. Those are exactly the folders GitSpawn needs.
- Trusting the prompt. Several agents ran the payload before their trust dialog appeared, so clicking "no" did not help.
- Stale agent versions. Fixes shipped in June, July and September. A team that pins an agent version for stability without a patch policy keeps the bug.
- Pins nobody verifies. A commit hash in a manifest feels like integrity. Without a check of
HEADafter checkout, it is a request, not a guarantee. - Plugins from self-hosted or Bitbucket remotes. Those are the hosts where a hash-named branch is possible.
- Unreviewed instruction files.
AGENTS.md,CLAUDE.mdand agent settings files arrive with the project and change what the agent does. Review them like code.
What to do this week
- Update every coding agent your team uses, and check the versions above against what is installed on laptops and CI images.
- If you use Grok Build or Claude Code's
ultrareviewon folders you did not clone, stop until the vendor ships a fix. - Add the pre-open check to onboarding for anyone who receives projects as archives, and run it on the folders already on disk.
- Audit plugin sources. Prefer plugins hosted where 40-character hex branch names are rejected, and turn off auto-update for plugins from other hosts until your agent verifies pins.
- If you build tools that call git, add the
-coverrides and the pin check, and run git probes inside your sandbox.
Frequently Asked Questions
Can opening a git repository run code on my machine?
Yes, if the folder carries its own .git directory. Git honors many settings in the repository's .git/config that name a program to run, and routine commands such as git status and git diff will run some of them. A plain git clone does not copy .git/config, so the risk comes from folders that arrive as files: zip archives, shared drives, sync folders and USB sticks.
What is the GitSpawn vulnerability?
GitSpawn is Manifold Security's name for a class of flaws, disclosed on 1 September 2026, where coding agents ran git commands for context without stripping the repository's own git configuration. A folder whose .git/config set core.fsmonitor to a program got that program executed when the agent ran git status, before any approval prompt. Manifold reported eight findings across seven agents, four of them unpatched at publication.
Which git config settings can execute commands?
The main ones are core.fsmonitor, core.hooksPath, core.sshCommand, core.pager, core.editor, sequence.editor, core.askPass, diff.external, diff and merge drivers, filter drivers declared through .gitattributes, credential.helper, gpg.program and aliases that start with an exclamation mark. Only a few security settings, such as safe.directory, are ignored when they appear in a repository's own config. Everything in this list is honored from .git/config.
Does git's safe.directory protect against this?
No. safe.directory only stops git from using a repository owned by a different user account. When you unzip a project into your own home directory, you own every file in it, so git trusts its config and runs whatever it names. It protects shared multi-user machines, not your own downloads.
How do I check a repository before opening it with a coding agent?
Read the folder's .git/config without running git inside it, and look for any setting that names a program, any non-sample hooks, nested or bare repositories, and agent or editor files that run commands. The script in this post does that in one pass. If anything shows up, open the folder only in a disposable VM or container with no credentials and no network.
What is Plugin4Shell and is my agent patched?
Plugin4Shell, disclosed by Air Security on 17 September 2026, is a flaw where coding agents installed a plugin at a pinned commit but never checked that the checkout landed on that commit. A branch named like the pinned hash could swap in different code, including through background auto-update. Air reports fixes in Claude Code 2.1.179 and Codex 0.146.0, no fix from GitHub Copilot, and no fix planned for the deprecated Gemini CLI.
Need a second pair of eyes on agent security?
Our work starts with opening other people's repositories, so this threat model is our own. Senior engineers review how your team receives code, runs coding agents and installs their plugins, then help you close the gaps, and we stay on the pager for what we build.
Sources
- Manifold Security, GitSpawn: A Single Flaw Lets Untrusted Repos Run Code in Claude Code, Codex, Cursor, and Grok, 2026-09-01
- Air Security, Plugin4Shell, 2026-09-17
- SonarSource, Arbitrary code execution and Claude Code CLI, 2026-04-30
- goose, GHSA-r5pp-p5r8-466r (CVE-2026-72718) and release v1.44.0, 2026-07-23
- Hermes Agent, pull request #101483, merged 2026-09-02, release v0.21.1, 2026-09-07, and CVE-2026-71963
- Qwen Code, pull request #11669, merged 2026-09-12, and release v0.23.4, 2026-09-14
- OpenAI Codex, pull request #34644 and release rust-v0.146.0, 2026-07-29
- Anthropic, Claude Code changelog, version 2.1.265, 2026-09-08
- Google Developers Blog, An important update: Transitioning Gemini CLI to Antigravity CLI, 2026-05-19
- Git, git-config documentation (core.fsmonitor, safe.directory, protected configuration) and githooks
- CVE Program, CVE-2022-24765, 2022-04-12
