DEV Community

Ashraf
Ashraf

Posted on

Plugin4Shell: Your AI Coding Agent's "Pinned" Dependency Was Never Actually Pinned

Your SHA pin is a suggestion, not a lock

You pin a plugin to a commit SHA because you did the review, you trust that exact code, and you never want it to silently change. That's the entire point of pinning. Last week, security researchers at AIR proved that four of the biggest AI coding agents — Claude Code, Codex, GitHub Copilot, and Gemini CLI — treat that pin as a polite suggestion instead of a hard constraint.

They're calling it Plugin4Shell, and the researchers are blunt about what it is: "the first supply chain vulnerability of the AI agent ecosystem." Zero-click, no user interaction required, and it hits tools that a huge chunk of the industry now runs with elevated trust and shell access.

The bug, in one sentence

Every one of these agents checks out the pinned commit — and then never verifies the checkout actually landed there.

That's it. That's the whole flaw. Verification theater: the pin looks honored, the hash is right there in the config, and the working tree is running something else entirely.

How you actually get owned

Vector 1 — branch name collision (Claude Code, Codex, GitHub Copilot)

These three run something functionally equivalent to:

git clone <plugin repo> ./
git checkout aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Enter fullscreen mode Exit fullscreen mode

Git resolves refs before it resolves raw object IDs. So if an attacker who controls the plugin repo creates a branch named exactly aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa and makes it the default, git checkout happily hands you the branch tip instead of the commit you pinned. Same hash string, completely different code.

Vector 2 — FETCH_HEAD confusion (Gemini CLI)

git clone --depth 1 <plugin repo> ./
git fetch origin 41d0bc0a4aeb2fbf797dacea39e876d98c95024b
git checkout FETCH_HEAD
Enter fullscreen mode Exit fullscreen mode

Name a branch FETCH_HEAD, make it default, and the checkout resolves to that branch instead of the commit you just fetched. Different mechanism, same failure: nobody diffed what actually ended up in the working directory against what was requested.

Why "zero-click" isn't marketing spin here

This is the part that should actually worry you. The attack chain is:

  1. Attacker publishes a clean, useful plugin pinned at commit aaa.... It passes review. People install it.
  2. Attacker ships a routine version bump to bbb.... Still clean. Trust builds.
  3. Attacker creates a branch literally named bbb... on their own repo, with malicious code as the default branch.
  4. Your agent's background auto-updater — on by default in Claude Code and Codex — re-runs the checkout logic against the new pin.
  5. You now have attacker-controlled code executing with your coding agent's permissions. You did nothing. You clicked nothing. You were probably in a meeting.

Prior AIR research already found 925 compromised "skills" reaching 134,000 agents through similar takeover patterns — this isn't a hypothetical, it's a repeatable playbook that just got a much bigger blast radius.

Patch status: pick your poison

Vendor Status Version
Anthropic (Claude Code) Patched 2.1.179+
OpenAI (Codex) Patched 0.146.0+
Microsoft (Copilot) Unpatched No fix shipped
Google (Gemini CLI) Deprecated, unpatched Users pointed to Antigravity CLI instead

Read that last row again. Google didn't patch it. Google killed the product and told everyone to migrate. If you have Gemini CLI installed anywhere, it is permanently exposed — there is no version number that fixes this, because there won't be one.

And if you're on Copilot: nearly 90% of Fortune 500 companies use it. That's not a niche exposure, that's most of corporate engineering running an agent with a known, public, unpatched RCE path.

The fix, and why it has to live in the agent

AIR's actual recommendation is embarrassingly simple — verify what you checked out:

test "$(git rev-parse HEAD)" = "<pinned-sha>" || abort
Enter fullscreen mode Exit fullscreen mode

One line. Run it after every checkout, before you trust anything in that working tree. The reason this has to be enforced by the agent itself — not the marketplace, not some registry-side scan — is that marketplace controls only see what was published. They can't see what ends up on your disk after your local git client resolves refs. The verification gap is entirely client-side, so the fix has to be too.

What to actually do this week

  • Check your installed agent versions right now. If you're running Claude Code or Codex, confirm you're on 2.1.179 / 0.146.0 or later.
  • If you're on Copilot or Gemini CLI, assume you're exposed. Disable plugin auto-update if you can, and manually audit what's actually checked out versus what your lockfile claims.
  • Add the verify-after-checkout line yourself to any internal tooling that does pinned git checkouts — this pattern isn't unique to AI agents, it's a general git footgun that just got a viral name.
  • Don't assume "SHA pinned" means "SHA verified" anywhere in your stack going forward. This bug exists because everyone assumed git's checkout semantics matched their mental model. They don't.

The uncomfortable meta-point: these agents run with real filesystem access, real shell access, and increasingly real production credentials. We spent years teaching developers to pin dependencies as a security baseline. Turns out the pin was never being checked. Audit your agent's supply chain like you'd audit npm's — because apparently you have to.

Top comments (0)