A dark wooden key board of brass hooks in a dim left-luggage room, each hook holding a single plain blank brass tag on a leather strap, one hook near the center holding two identical tags doubled up and lit by a warm red-orange highlight, pigeonhole slots of worn leather cases behind with one slot standing empty, photoreal

Four Coding Agents Pin Plugins to a Commit They Never Verify. The Marketplace You Host Yourself Is the Exposed One.

All four major coding agents ask git for a pinned commit and never check that the checkout landed on it, because git prefers a ref over an object id when a name could be either.

The default marketplaces mostly held because GitHub refuses to create branch names shaped like commit hashes, not because any agent verified anything. Bitbucket and self-hosted git allow those names, and both are documented marketplace backends.

No CVE was assigned and no vendor published an advisory, so your scanner has nothing to match on. Claude Code 2.1.179 and Codex 0.146.0 carry the fix. Copilot and Gemini CLI do not.

Two lines of git

Air Security published Plugin4Shell on 17 September. The bug lives in how four coding agents install a plugin. Claude Code, Codex and GitHub Copilot all run something close to this:

git clone <plugin repo> ./
git checkout aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

That second line looks like it pins the install to a reviewed commit. It does not. Git carries an old ambiguity rule: when a name is both a valid ref and a valid object id, git prefers the ref. If the repository holds a branch whose name is the same forty hex characters as the commit you asked for, the checkout lands on the branch. None of the four agents checked afterward that HEAD ended up where they sent it.

Gemini CLI takes a different route to the same place. It clones shallow, fetches the commit, then runs git checkout FETCH_HEAD. A repository whose default branch is named FETCH_HEAD resolves that checkout to the branch instead of the fetched object.

Four teams, four codebases, one missing line: compare what came back to what you asked for.

What the pin was carrying

The marketplace review is the front door. Someone reads the plugin, approves it, records the commit SHA. Every install after that is supposed to be that exact reviewed code. That pin is the entire supply chain control, and it is the same model as a lockfile, a pinned image digest, or a vendored dependency.

Plugin4Shell does not forge the pin. The marketplace still shows the correct SHA. The client still asks for the correct SHA. The working tree just holds different code. As Air put it, only an agent-side fix restores the guarantee, because the resolution happens on the client. There is nothing a marketplace can do about it from its side.

A plugin is not sandboxed from the developer running it. It gets the same access that person has: their files, their saved credentials, their SSH keys, the internal repos and production systems they can log into. The same researchers previously traced a malicious plugin to more than 26,000 agents, and a skill-hijacking campaign to 134,000. The blast radius here is not theoretical.

Most people were fine, and it was not the agent that saved them

GitHub rejects branch and tag names that look like commit hashes. Anthropic's community catalog, the default Claude Code marketplace and the Copilot marketplace all point at GitHub repositories. So the ordinary path, installing from the built-in catalog, was protected by a naming restriction at a third party.

That is the part worth sitting with. Four independent vendors shipped the same absent check. What kept it from becoming a mass event was a constraint none of them wrote, enforced on a platform they do not control, which happened to be the one they all defaulted to. Nobody designed that defence. It was inherited.

Gemini CLI does not even get that inheritance. GitHub's hash-name rule does not clearly cover a branch named FETCH_HEAD, so that variant survives on GitHub too. Google's answer, on 4 August, was that Gemini CLI is deprecated and users should move to Antigravity.

Where both halves line up

Zero-click needs two things at once: a git host that permits hash-shaped refs, and plugin updates that run without anyone watching.

Auto-update is on by default in Claude Code and Codex, but only for their own built-in marketplaces, which live on GitHub. For outside marketplaces it is off or optional. On GitHub the branch trick does not work. So the fully automatic version of this attack needs an outside marketplace with unattended updates switched on.

Anthropic's own documentation lists Bitbucket and self-hosted git as valid marketplace backends. That is the enterprise configuration. You did not want your developers pulling plugins from a public catalog, so you stood up an internal marketplace on the git server you already run, curated what goes in it, and turned on unattended updates because you control the repos and you trust them.

Every one of those decisions was defensible, and together they remove the accidental guard rail and switch on the automation. Your internal marketplace has no GitHub naming rule in front of it. Worse, the trust chain now extends past review to whoever can push a branch to that repository afterward, which includes an author who changes their mind and anyone who takes over the repo later.

The teams most likely to be fully exposed are the ones that took plugin supply chain seriously enough to build their own catalog.

A version number is the only signal you get

The timeline is unusually long. Air found it in May with a working proof of concept and notified all four vendors in June. Anthropic's fix was verified on 17 June in Claude Code 2.1.179. OpenAI's was verified on 12 August in Codex 0.146.0. Google declined to fix on 4 August. Microsoft has shipped nothing for Copilot. Public disclosure came on 17 September, three months after the last vendor patch landed.

There is no CVE. None of the four published a security advisory. That means nothing lands in your dependency scanner, nothing appears in an SBOM diff, nothing shows up in the feeds your security team subscribes to. The only way to know where you stand is to read a version string off every machine running the agent.

That inventory is more annoying than it sounds. Coding agents install per user, update on their own schedule, and also sit baked into CI images and devcontainers where they are pinned and therefore stale. The pinned copy in your build image is the one nobody thinks to check.

The short list

Read the client version everywhere the agent runs, laptops, CI images and devcontainers included. You want Claude Code at 2.1.179 or later and Codex at 0.146.0 or later.

Inventory your marketplaces by host, not by name. The question is which git server serves each one, not who curates it.

Turn off unattended updates for any marketplace that is not on GitHub until the client is patched. On Copilot, avoid plugins from non-GitHub repositories entirely, because there is no patch to wait for. On Gemini CLI there is no fix coming at all.

Updating the client does not remove a plugin that already swapped. If untrusted code ran under a developer's account, rotate what that account could reach rather than assuming the upgrade cleaned up after it.

Pinning is a claim, not a check

Setting the bar is specifying what should happen. Holding the bar is confirming it did. A pinned SHA sets the bar beautifully and holds nothing on its own, and for four agents across at least four months, nobody was holding it.

The useful move is not to patch this one and move on. It is to walk your stack and ask, of every pin you have, who compares what came back against what was asked for. Container digests you pull but never re-verify. Lockfiles a build step can silently regenerate. Skill archives you unpack without checking a digest, a gap Microsoft closed in Agent Framework 1.19.0 on 19 September by restricting skill archives to ZIP files with verified digests.

Most of those pins are probably fine. The ones that are not will look exactly like the ones that are, right up until someone names a branch after a commit.