Last week I pinned a Claude Code plugin to a specific commit SHA, the way I always do when I don’t fully trust a third-party repo. Forty hex characters, immutable, supposedly un-spoofable. Three researchers at AIR Security — Or Nevo, Dor Granat, Niv Hoffman — just published a paper proving that assumption wrong for four separate agents, and the fix isn’t the story you’d expect from a normal CVE.

The bug is called Plugin4Shell. Once you see the mechanism, it’s almost insulting how simple it is.

The one line of Git behavior nobody double-checks

Claude Code, OpenAI Codex, GitHub Copilot, Gemini CLI — every one of them locks a plugin to a commit hash, then runs something equivalent to git checkout <SHA> before executing it. That’s the whole safety model: pin the hash, get exactly that code, full stop.

Except Git doesn’t resolve refs the way that mental model assumes. Hand Git a 40-character string and it doesn’t go straight to the object database. It checks branches and tags first. If a branch happens to be named the same 40 hex characters as the commit you pinned, Git checks out the branch. Not the commit.

So an attacker who controls the plugin repository creates a branch literally named after your locked SHA, points that branch at whatever code they want, and every agent that “pinned” your plugin runs their version instead of yours. Gemini CLI has its own flavor of the same mistake: it trusts FETCH_HEAD, which an attacker can repoint just as easily. No jailbreak. No prompt injection. No social engineering. You did everything a careful engineer is told to do, and the safety mechanism itself was the lie.

I’ve spent fifteen years telling junior engineers that pinning to a SHA is the responsible move, full stop, end of discussion. This is the first time I’ve had to add an asterisk to that sentence.

Four vendors, four different responses

What caught my attention wasn’t the bug — vulnerabilities happen — it’s what each vendor did after AIR Security reported it.

Claude Code shipped a fix in 2.1.179. Codex fixed it in 0.146.0. Both closed the ref-ambiguity hole the same way: resolve to the object directly, ignore any ref that happens to share the string.

GitHub Copilot is still unpatched as of this writing.

Google didn’t patch Gemini CLI at all. They deprecated it.

I keep coming back to that last one. Deprecating a product is a legitimate response to a vulnerability — sometimes the honest fix really is “we’re not maintaining this anymore” — but it also means every engineer who adopted Gemini CLI on the promise of pinned-plugin safety is now holding a tool with a known, permanently unpatched supply-chain hole unless they’ve already migrated off it. If your team is still running it, that migration just stopped being optional.

The blast radius isn’t uniform either. GitHub’s own plugin marketplace blocks hash-like branch names, so installs from github.com aren’t exposed through this specific vector. Bitbucket and self-hosted Git have no such guardrail. If your org runs an internal plugin registry on self-hosted infrastructure — and a decent chunk of the enterprise teams I’ve consulted for do exactly that — you inherited this risk the moment someone flipped on auto-update.

Auto-update is what turns a theoretical bug into a zero-click one. Claude Code and Codex both auto-update plugins by default. You don’t run an install command for the malicious branch to land — the agent fetches it on schedule and reports back that the “locked” version is installed, because as far as its own logic is concerned, it is.

What I actually changed this week

I turned off plugin auto-update on every Claude Code instance I run. I checked our internal plugin repos for unexpected branch activity — nothing, for what it’s worth. And I added one line to our plugin-vetting checklist: verify the pin resolves to a commit object, not just that a SHA-looking string sits in the config.

That check is almost insultingly cheap — git cat-file -t <SHA> before you trust anything, five seconds, done. The reason it wasn’t already there is that “pinned to a SHA” has functioned as shorthand for “safe” in every code review I’ve sat through for a decade. Plugin4Shell is a reminder that the shorthand was doing work the underlying mechanism never promised.

This isn’t really a story about Git. Agent tooling inherited a decade of assumptions about what “pinning a dependency” means, built on infrastructure — package managers, VCS ref resolution — that was never designed with an autonomous, auto-updating consumer sitting on top of it. Every one of those assumptions is worth re-auditing before you point an agent at it, and I doubt Plugin4Shell is the last one we find this way.

Export for reading

Comments