For about a year, the answer to “what stops a coding agent plugin from turning malicious after you approved it?” has been short and confident: the marketplace pins it. Each plugin is locked to a 40-character commit hash, the hash identifies one exact snapshot of the code, and the agent installs that snapshot and nothing else. If the author pushes something new, the pin doesn’t move until the marketplace moves it.
It is a good control, and it’s the same idea the package ecosystems took a decade of incidents to arrive at. Last month it turned out that four of the most widely used coding agents had been checking the pin without checking that they had actually used it.
What was disclosed
In mid-September, the research team at AIR Security published Plugin4Shell, a flaw in the plugin systems of Claude Code, OpenAI Codex, GitHub Copilot and Google’s Gemini CLI. They found it in May, built working proofs of concept, and reported it to all four vendors in June. It went public three months later, on 17–18 September depending on whose timestamp you read.
Their one-line summary is the whole story: every one of the four agents “checks out the pinned commit without verifying the checkout landed there.”
There is no claim of exploitation in the wild, and no reported breach, compromise or data loss. The Hacker News put it plainly: “no sign it has been used in a real attack.” This post is about a control that didn’t hold, not about an incident. The name invites comparisons with Log4Shell. The scale doesn’t.
Where it broke
If you know git but haven’t met this trick, here it is.
When you ask git to check out a string, it has to decide what that string means. A 40-character hex string could be a commit hash. It could also be the name of a branch, because git lets you give a branch almost any name, including one made of 40 hex characters. When a name can be read both ways, git prefers the reference. The branch wins.
So the owner of a plugin repository creates a branch whose name is exactly the pinned hash and points it at different code. The marketplace says “install commit a3f9…”, the agent runs a checkout of a3f9…, and git hands back the branch, not the commit. The agent compares the pin it was given with the pin it asked for, finds they match, and carries on. It never asks what is actually on disk.
Gemini CLI fails the same way by a slightly different route: its checkout can be made to resolve to a branch named FETCH_HEAD instead of the commit it just fetched.
Why “zero-click”
If this only happened on first install, it would be an ordinary supply-chain risk: you approve a plugin, and you are trusting its author at that moment.
What makes it worse is that the same checkout runs again whenever the plugin updates. In Claude Code and Codex, background auto-update is on by default for the built-in marketplace. When the marketplace bumps a pinned hash, the update reaches plugins that are already installed, with no prompt, no approval and no reinstall. Copilot’s auto-update for outside marketplaces is off or optional, which narrows its exposure without closing it.
A plugin runs with the developer’s permissions. That covers the local source tree, cloud credentials in the environment, SSH keys, and whatever internal repositories and services that machine can reach. The attacker in this scenario doesn’t need to phish anyone. They need to control one plugin repository on a host that allows the right branch names, and then wait for the next update.
Four vendors, four answers
This is the part a CTO actually has to deal with, because the four vendors didn’t respond the same way.
- Anthropic: patched. Claude Code 2.1.179, confirmed fixed in June.
- OpenAI: patched. Codex 0.146.0, verified in August. OpenAI’s fix note gives the cause directly: git “can interpret a requested commit SHA as a branch name.”
- Microsoft / GitHub Copilot: no client-side fix. GitHub’s position is that its platform rejects branch and tag names that look like full commit hashes, so the demonstrated variant can’t be staged on a GitHub-hosted repository. That’s true, and it’s a meaningful mitigation. But it protects the host, not the agent. A plugin sourced from Bitbucket or a self-hosted git server gets none of it, and AIR disputes how far the mitigation reaches.
- Google / Gemini CLI: not patched, being retired. Google is deprecating Gemini CLI rather than fixing it and pointing users to Antigravity, its newer agent. According to AIR, Antigravity is out of this attack’s reach, but only because it has no marketplace hash pinning to bypass, not because the flaw was fixed. Gemini CLI installs that are still in use stay exposed.
As of 5 October we found no fix announced for Copilot or Gemini CLI. If that has changed by the time you read this, good. The point below still stands.
So two vendors fixed it, one argued that its own hosting makes a fix unnecessary, and one dropped the product. Each position has a reason behind it. Together they mean “is it patched?” is no longer one question. It’s a question for each vendor and each git host, and the developers running these tools can’t answer it for you, because most of them don’t know which host their plugins come from.
Checking the request, not the result
It’s worth naming the pattern, because it is much older than coding agents.
The pin was an integrity control, and it was checked in the wrong place. The agents verified the instruction (“install this hash”) and never verified the outcome (“this hash is what’s now on disk”). The fix is one comparison after the checkout: read back the commit you landed on and confirm it is the one you asked for. Two vendors added that line. Before that, the most important assurance in the plugin model rested on an assumption about how git resolves names that nobody had written down.
This is the same mistake as a deployment pipeline that confirms the image tag it pushed but never checks the digest that is actually running. It’s a backup job that logs “completed” without anyone ever restoring from it, or a firewall change ticket closed because the change was submitted, not because traffic changed. In each case the system checks that it asked for the right thing and treats that as proof it got the right thing.
A pin is a claim. A claim is only as good as the last time someone compared it with the artefact. Wherever you rely on an integrity control, ask whether it checks the request or the result. If it checks the request, you have a well-documented intention, not a control.
There is a broader point too. Agent plugin marketplaces arrived with the trust model of package managers and none of their history. npm, PyPI and the rest earned their defensive habits the hard way: lockfile integrity hashes, provenance attestations, install-script restrictions. The agent ecosystems took the shape of a package manager and skipped that history. They pin, but until last month at least four of them didn’t verify. I’d expect more findings of this shape, not fewer.
The inventory
None of this calls for a maturity model. It calls for a short message on Monday morning, and someone accountable for the answers:
- Which coding agents run in our organisation? Count the ones engineers installed themselves, not just the ones we bought.
- Which plugins do they load, and from which marketplaces? Built-in, third-party, internal.
- Where are those plugin repositories hosted? GitHub, Bitbucket, self-hosted git. This decides whether GitHub’s mitigation applies to you.
- Is auto-update on? If so, who decides when a plugin changes: us or the marketplace?
- What version is each agent on? For Claude Code and Codex, are we at or above the fixed releases? For Copilot and Gemini CLI, what is the plan, given no fix is coming or none has arrived?
- What can a plugin reach on a developer’s machine? Credentials in the environment, keys, repositories, production access.
- Who would notice a swap? If a plugin’s code changed underneath us tomorrow, which signal would tell us, and who would see it?
If the last answer is “nobody,” that is your finding, whatever the other answers say.
Plugin4Shell wasn’t a sophisticated attack. It was one missing comparison in a control everyone pointed to. Most of the controls you rely on have not been tested the way this one just was. Pick one and check what it actually verifies.