Plugin4Shell explained: how a git branch named like a commit hash beats plugin pinning in Claude Code, Codex, Copilot and Gemini CLI, with a working reproduction
AIR's post explains what its researchers found and who has patched. This page shows the git behavior underneath it in a scratch repository you can rebuild in two minutes, shows the warning git prints that no agent reads, gives the one-line checkout form that is immune, and checks the 'auto-update is the default' claim against Claude Code's own docs.
Read the original first: https://www.air.security/blog-posts/plugin4shell
In one minute
- On September 17 the security firm AIR disclosed Plugin4Shell. Claude Code, Codex, GitHub Copilot and Gemini CLI all pinned plugins to a commit hash and then never checked that the checkout landed on that hash.
- The trick is a git rule: when a name is both a branch and a commit id, git prefers the branch. Name a branch with the pinned 40-character hash, make it the repo's default branch, and 'git checkout <hash>' lands on the branch instead.
- We reproduced it on git 2.49.0. Git prints 'warning: refname ... is ambiguous' and checks out the branch anyway. Agents read exit codes, not warnings.
- GitHub refuses 40-hex branch names, so GitHub-hosted marketplaces are not exposed. Bitbucket and self-hosted git servers allow them. Claude Code's docs list both as supported marketplace sources.
- Zero-click comes from background auto-update. AIR says that is the default in Claude Code and Codex. Claude Code's docs say official Anthropic marketplaces auto-update by default and third-party ones do not. Both can be true: the risky case is a third-party, non-GitHub marketplace with auto-update turned on.
- Claude Code fixed it in 2.1.179 (June 17). Codex fixed it in 0.146.0 (verified August 12). Microsoft has not shipped a Copilot fix. Google says Gemini CLI is deprecated and will not be patched.
- The immune checkout is 'git checkout --detach <hash>^{commit}' followed by a check that 'git rev-parse HEAD' equals the pin. Two lines.
What AIR found
A plugin marketplace for a coding agent is a catalog. Each entry points at a git repository and, in the careful ones, a commit hash. The hash is the pin. The idea is that a reviewer looked at exactly that commit, so whatever the agent installs later is exactly what was reviewed.
AIR's researchers found that all four major agents did the pin check the same lazy way. They cloned the repo and ran a checkout of the hash. They confirmed the hash existed. They never confirmed that the working tree they ended up with was that hash. AIR's phrasing: the agents 'check out the pinned commit without verifying the checkout landed there.'
That gap is only exploitable because of a git behavior most people never hit. Git lets you name a branch with 40 hex characters. And git's own documentation says that when a name is both a valid ref and an object id, git prefers the ref. So a branch named after the good commit, pointing at a bad commit, wins.
Three of the four agents ran 'git clone' then 'git checkout <sha>'. Gemini CLI did 'git fetch origin <sha>' then 'git checkout FETCH_HEAD', which fails the same way if the default branch is literally named FETCH_HEAD.
The reproduction, in a scratch repo
This is the whole thing on a Mac with git 2.49.0. No agent involved. The 'remote' is a local directory, which is enough because the bug is in the client's checkout, not in transport.
mkdir -p p4s/remote && cd p4s/remote && git init -q git commit -q --allow-empty -m good GOOD=$(git rev-parse HEAD) echo EVIL > plugin.txt && git add plugin.txt && git commit -qm evil EVIL=$(git rev-parse HEAD) # the attack: a branch NAMED after the good hash, POINTING at the evil commit, # and made the repository's default branch git branch "$GOOD" "$EVIL" git symbolic-ref HEAD "refs/heads/$GOOD" # the agent's side cd .. && git clone -q remote work && cd work git checkout "$GOOD" git rev-parse HEAD ls
Output, trimmed:
warning: refname '0a687e51e27907e29c5af41ade49da16dadea8e5' is ambiguous. Git normally never creates a ref that ends with 40 hex characters because it will be ignored when you just specify 40-hex. These refs may be created by mistake. ... 0e80e7ee86863f1a216dbdcb306fcf23fa25ca74 plugin.txt
The pin was 0a687e51. HEAD is 0e80e7ee. The working tree holds plugin.txt, the file that only exists in the evil commit. The checkout exited 0. Git told us in plain English that the name was ambiguous, on stderr, and then did the wrong thing anyway.
That last part is why this is an agent bug and not a git bug. A human at a terminal would see the warning. An agent's installer sees an exit code of zero and moves on. Warnings on stderr are not part of the contract these tools were written against.
Why the branch has to be the default branch
Our first attempt did not reproduce. We created the hash-named branch but left the default branch alone. After the clone, 'git checkout <hash>' landed on the right commit and the pin held.
The reason is what 'git clone' creates locally. It creates one local branch, the remote's default, and tracks everything else as 'origin/<name>'. A remote-tracking ref named 'origin/0a687e51...' does not collide with the bare hash, so git resolves the hash to the commit. Only when the hash-named branch is the default does a local branch with that exact name exist, and only then does the ref win.
AIR's attack chain says the attacker makes the malicious branch the repository default. Our test confirms that step is load-bearing, not cosmetic. It also tells you something useful for defense: if a marketplace's install path fetches a single ref instead of cloning, the exposure is different, which is exactly the Gemini CLI variant with FETCH_HEAD.
Why GitHub is safe and Bitbucket is not
GitHub rejects branch names that are 40 hex characters. You cannot push one. So a plugin marketplace hosted on GitHub cannot carry this trap, no matter how the agent checks out.
Bitbucket and ordinary self-hosted git servers accept the name. Git itself accepts it too. 'git check-ref-format --branch <40-hex>' passes. AIR points out that Anthropic's documentation lists Bitbucket and self-hosted servers as supported marketplace sources, and we confirmed the current docs still say: 'Git URLs: any git repository URL, including GitLab, Bitbucket, and self-hosted servers.'
So the exposed population is narrower than 'everyone with plugins.' It is everyone whose agent installs plugins from a marketplace that is not on GitHub, on an unpatched agent version. That is still a lot of enterprises, because the whole point of a self-hosted marketplace is internal review and control.
The zero-click part: what AIR says versus what the docs say
AIR's headline claim is that no click is needed because agents update installed plugins in the background, and 'in Claude Code and Codex this is the default.' The chain: publish a clean plugin, get it pinned, wait for the marketplace to re-pin to a new commit, then create the hash-named branch pointing at malware. The next background update pulls it.
Claude Code's marketplace docs say something more specific. 'claude-plugins-official, most other official Anthropic marketplaces, and marketplaces added from claude.ai have auto-update enabled by default. Other third-party marketplaces and local development marketplaces have auto-update disabled by default.' Anthropic's official marketplaces live on GitHub.
Both statements can be true at once, and the overlap is the actual danger zone: a third-party marketplace, hosted on Bitbucket or a self-hosted server, where someone turned auto-update on, or where an administrator set autoUpdate to true in extraKnownMarketplaces for the whole organization. That last one is a normal thing for an enterprise to do. It is one JSON key.
We could not check Codex's default from its docs on this pass, so for Codex we are relying on AIR's statement. For Copilot, AIR says the plugin path is exposed and Microsoft has not shipped a fix. For Gemini CLI, Google told AIR on August 4 that the product is deprecated and will not be patched, and AIR suggests moving to Antigravity.
The fix, and the two lines that would have prevented it
AIR is right that no marketplace can enforce this. A marketplace can only publish a hash. Honoring it is the client's job. The patched agents now verify the checkout result.
If you write anything that installs code from a pinned git commit, this is the immune form:
git checkout --detach "${PIN}^{commit}"
[ "$(git rev-parse HEAD)" = "$PIN" ] || { echo 'pin mismatch'; exit 1; }The ^{commit} suffix tells git to peel the name to a commit object, and in our test it resolved to the pinned commit even with the hostile branch in place. Git still printed the ambiguity warning, which is fine. The second line is the one that matters. Compare what you got to what you asked for. If they differ, stop.
You can also refuse the trap at the door. 'git ls-remote --heads <url>' lists remote branches. A branch name matching ^[0-9a-f]{40}$ has no honest reason to exist. Reject the marketplace.
Timeline
- May 2026: AIR finds the bug with working exploits against all four agents.
- June 2026: coordinated disclosure to Anthropic, OpenAI, Microsoft and Google.
- June 17: Anthropic confirms the fix, Claude Code 2.1.179.
- August 4: Google confirms it will not patch Gemini CLI, citing deprecation.
- August 12: OpenAI's Codex fix, 0.146.0, verified.
- September 17: public disclosure. Copilot still unpatched.
Claude Code releases this week are in the 2.1.27x range, so anyone on a current build is past the fix. The check still matters for pinned enterprise installs.
Who is affected
| Case | Status |
|---|---|
| Claude Code with plugins from a marketplace on Bitbucket, GitLab or a self-hosted server | Fixed in 2.1.179 and later. Check claude --version. GitHub-hosted marketplaces were never exposed. |
| Codex with plugins from a non-GitHub marketplace | Fixed in 0.146.0 and later. Check codex --version. |
| GitHub Copilot plugins from a non-GitHub marketplace | No fix shipped as of September 18. Turn off background plugin updates and review marketplace sources. |
| Gemini CLI | Deprecated. Google will not patch. Move off it. |
| Any marketplace hosted on GitHub | Not exposed. GitHub blocks 40-hex branch names. |
| Your own scripts that install code from a pinned git commit | Exposed if they run 'git checkout <sha>' without comparing HEAD to the pin afterward. |
What to do
- Run claude --version and codex --version today. You want at least 2.1.179 and 0.146.0.
- In Claude Code, run /plugin marketplace list. Anything not on github.com is the list to review.
- Open /plugin, Marketplaces, and note which non-GitHub marketplaces have auto-update on. Ask who turned it on and why.
- If your organization sets autoUpdate: true in extraKnownMarketplaces, confirm every one of those marketplaces is on GitHub or on a patched agent everywhere.
- Copilot teams: disable background plugin updates from non-GitHub marketplaces until Microsoft ships a fix.
- Gemini CLI users: migrate. There is no patch coming.
- Run git ls-remote --heads against each non-GitHub marketplace repo you use. A branch named with 40 hex characters is a red flag. Report it.
- In any installer you own, add the HEAD-equals-pin check after checkout. It is one line.
What is still unknown
- No CVE number appears in AIR's post or in Help Net Security's coverage as of September 18.
- AIR says background auto-update is the default in Claude Code and Codex. Claude Code's docs say third-party marketplaces do not auto-update by default. We could not check Codex's documented default on this pass, so the Codex claim rests on AIR alone.
- We do not know how many real marketplaces are hosted on Bitbucket or self-hosted servers. AIR says 'millions of agents' are affected but does not publish the count or method.
- Microsoft has not stated a timeline for a Copilot fix, and we have not seen a Microsoft statement at all beyond AIR's account.
- Our reproduction uses the clone-then-checkout sequence AIR describes. We did not test against the agents' actual installer code, so the exact commands each agent ran before patching rest on AIR's write-up.
- AIR sells a marketplace product and notes its own customers were not affected. That does not change the git behavior, which anyone can reproduce, but it is context.
Sources
- AIR, September 17, 'Plugin4Shell - Zero Click RCE Vulnerability found in top 4 most popular coding agents' by Or Nevo, Dor Granat and Niv Hoffman — the original report
- Help Net Security, September 18, 'Zero-click RCE vulnerability hit four major AI coding agents, two remain unpatched'
- Claude Code docs: Discover and install prebuilt plugins through marketplaces (sources and auto-update defaults)