Checked Out, Not Checked: Inside Plugin4Shell's Zero-Click Hit on Four AI Coding Agents

I came across this one in my news stream and it’s a genuinely good bug, not because the mechanism is exotic but because it isn’t. Security researchers at AIR Security disclosed a vulnerability they’ve named Plugin4Shell, a zero-click remote code execution affecting plugin installation in four major AI coding agents: Claude Code, OpenAI Codex, GitHub Copilot, and Gemini CLI. There’s no CVE number attached as of this week, and no evidence of real-world exploitation. There’s also nothing cryptographic to break. The bug is a git ref ambiguity that’s been sitting in plain sight in how these agents install and update plugins.

What a SHA pin is supposed to guarantee

Plugin marketplaces for these agents pin a plugin to a specific commit hash, not a branch or a tag. That’s a deliberate choice. A branch can move. A tag can be force-pushed over. A commit hash, in theory, can’t, it’s a checksum of the content itself, so pinning to one is supposed to mean the exact code a maintainer reviewed is the exact code that gets installed, today and on every future update, until someone deliberately re-pins to a new hash.

That guarantee only holds if the installer actually confirms it landed on that commit. AIR Security’s finding is that none of the four agents do.

The gap: checked out, never checked

The install and update flow in all four agents runs a git checkout <sha> against the plugin’s repository and then trusts the result. What none of them do afterward is run something like git rev-parse HEAD to confirm the working tree actually resolved to that exact commit object. The pin gets read, the checkout command gets issued, and the agent moves on. Whether the checkout actually landed where it was told to is never verified.

Git gives an attacker a way to exploit exactly that gap, because git checkout <name> doesn’t always mean what it looks like it means. Git resolves a ref name through a preference order, and depending on the exact command, a branch or tag can take priority over a raw object ID when both exist with the same name. If a repository has a branch literally named after the pinned commit hash, git checkout <that hash> can resolve to the branch tip instead of the commit object. The pin string in the marketplace manifest never changes. The code that ends up on disk does.

Two ways to win the same race

Claude Code, Codex, and GitHub Copilot share one variant of this: an attacker who controls the plugin’s repository creates a branch whose name is identical to the currently pinned SHA, and points that branch at different code. The next checkout against that pin resolves the branch instead of the commit.

Gemini CLI has a related but distinct version. Its install sequence runs git checkout FETCH_HEAD as part of pulling the latest pinned state. FETCH_HEAD is itself just a ref name, and a repository can have an actual branch named FETCH_HEAD. When one exists, the checkout resolves to that branch instead of whatever was genuinely just fetched.

Neither variant needs a cryptographic collision, a supply chain compromise of a package registry, or a stolen signing key. It’s a naming trick against how git resolves ambiguous refs, sitting underneath a security control that was never checking whether its own precondition still held.

Why it needs no click at all

What makes this zero-click rather than “a malicious plugin you’d have to knowingly install” is the update path. All four agents update installed plugins automatically in the background. The sequence AIR Security lays out: an attacker publishes a plugin that’s genuinely benign, gets it installed by real users, and waits. When the maintainer ships a routine, still-benign update, the marketplace re-pins everyone to the new hash. At that point the attacker creates a branch named after the new hash and points it at malicious code. The next silent auto-update, on every instance that has that plugin installed, runs the checkout again, and this time it resolves to the branch. Code executes with the user’s own local privileges. Nobody clicked anything, approved anything, or saw a prompt, because from the agent’s point of view a routine background update just completed exactly as it was supposed to.

Where each vendor stands

Coordinated disclosure ran on a fairly normal timeline: AIR Security found the bug with a working proof of concept in May 2026 and notified all four vendors in June. Patch status as of this week:

VendorStatus
Anthropic (Claude Code)Patched, version 2.1.179
OpenAI (Codex)Patched, version 0.146.0
Microsoft (GitHub Copilot)No fix shipped
Google (Gemini CLI)Not patching, product being deprecated in favor of Antigravity

If you’re running Claude Code or Codex, updating closes this off. If you’re running GitHub Copilot’s agent features or Gemini CLI with third-party plugins installed, there’s currently no vendor fix to apply, only your own judgement about which plugins you trust and whether background auto-update is worth leaving on until one arrives.

The same shape as the last integrity story here

This is the second time in a couple of weeks I’ve written about a security control that exists, runs, reports success, and still doesn’t verify the one thing it was built to verify. The Ted backdoor in HAProxy worked because a recompiled binary reports the identical version string as a clean one, so a version check tells you nothing about whether the binary you’re actually running matches the one that was supposed to ship. Plugin4Shell is the same failure shape one layer up the stack: the pin looks honored, the manifest string never changes, and the thing that’s actually supposed to make the pin meaningful, confirming the checkout landed where it claimed to, was never wired in.

It’s also worth sitting with for a minute given how much of this site now gets written and deployed through Claude Code rather than by hand. I don’t run third-party plugins against this blog’s own MCP tooling, and this specific bug needs a plugin marketplace to exploit, so it isn’t a direct exposure here. But the general lesson generalizes past these four products specifically. If you ever pin a dependency to a commit hash in your own automation, whether that’s a deploy.yml, an Ansible role pulling from a git source, or a shell script doing exactly what these agents do, checking out a SHA is not the same thing as confirming you landed on it. git rev-parse HEAD after the checkout, compared against the hash you pinned, is one extra line and it’s the line that was actually missing here.

What to actually do

Update Claude Code and Codex if you haven’t already, the fixed versions are above. If you’re on GitHub Copilot or Gemini CLI and use plugins from anything other than a marketplace you’d genuinely trust with code execution on your machine, turning off auto-update until a fix ships is the only lever available right now, since the checkout logic itself can’t be worked around from the outside. And if any of your own tooling pins by commit hash the way these agents do, it’s worth a five-minute check on whether you’re verifying the checkout the same way you’re trusting the pin.

Sources: AIR Security: Plugin4Shell · The Hacker News: Plugin4Shell Lets Repository Owners Swap Pinned Plugin Code Across Four AI Coding Agents · Help Net Security: Zero-click RCE vulnerability hit four major AI coding agents, two remain unpatched · Cybersecurity News: Plugin4Shell Zero-Click RCE Hits Claude Code, Codex, Copilot and Gemini CLI