Plugin4Shell: A Zero-Click RCE in Four Coding Agents, and the Pin That Never Checked

·12 min read·Evergreen Tools Team

On September 18, 2026, security firm Air Security disclosed a class of vulnerability it calls Plugin4Shell, and what it breaks is SHA pinning: the mechanism that locks an installed plugin to a specific, previously reviewed commit. Four of the most widely used coding agents are affected, Anthropic's Claude Code, OpenAI's Codex, Microsoft's GitHub Copilot, and Google's Gemini CLI. Air calls it the first supply-chain vulnerability of the AI agent ecosystem, and it summed up the uncomfortable part in one line: the victim does not have to install plugins carelessly. They only have to have a plugin installed, from a marketplace they trust, that was reviewed and pinned exactly as the security model intends.

Supply chain security and verification

A pin only means something once it is verified

1. The Defect: The Pin Was Checked Out, Never Verified

The four agents share the same category of bug. Each retrieves a pinned commit hash and instructs Git to check it out, and none of them verifies afterward that the resulting working tree actually landed on that hash. Air found the issue in May 2026 with working proof-of-concept exploits against all four agents, and privately reported it to each vendor the following month. Three months later, at public disclosure, no CVE identifier had been assigned. The remediation record is uneven. Anthropic patched Claude Code in version 2.1.179. OpenAI patched Codex in 0.146.0. Microsoft had shipped no fix for Copilot at disclosure. Google went further in the other direction, deprecating Gemini CLI in favor of its successor Antigravity rather than patching it, which leaves every existing install exposed indefinitely.

# The defect, simplified. The agent fetches the pin, checks it out,
# and never confirms what actually landed on disk.
git clone https://host/plugin.git && cd plugin
git checkout "$PINNED_SHA"      # Git's ref resolution decides the meaning
# ...and installs whatever is now in the working tree, trusting the pin.

# There is no post-checkout assertion. No mismatch error. No signal.
# Four independent implementations shared this same gap.

2. Two Variants: One Borrows a Branch Name, One Borrows FETCH_HEAD

The first variant affects Claude Code, Codex, and GitHub Copilot. The agent clones the plugin repository and runs git checkout against the pinned 40-character hash. If an attacker controls that repository, they can create a branch whose name is identical to that hash and set it as the repository's default branch. When the branch name and the raw commit object are ambiguous, Git's reference resolution can favor the branch, so the checkout silently lands on attacker-controlled branch content instead of the immutable commit the pin was meant to enforce. The second variant affects only Gemini CLI, which runs git fetch origin followed by git checkout FETCH_HEAD. If the attacker names the repository's default branch FETCH_HEAD, the same ambiguity appears in reverse: the checkout resolves to the branch rather than the commit that was just fetched, discarding the legitimate pinned code entirely. Both variants require the attacker to control the plugin's source repository, which is achievable either by publishing a plugin that is benign at review time or by taking over an already-trusted repository later.

# The attack. The attacker controls the plugin repository.
# Create a branch whose NAME is the pinned 40-character hash,
# and make it the repository's default branch.
SHA=$(git rev-parse HEAD)             # the hash the marketplace will pin
git checkout -b "$SHA"                # a branch that looks like a commit
git commit -am "routine maintenance"  # now it carries malicious code
git push origin "$SHA"

# On hosts that allow a 40-hex branch name (Bitbucket, self-hosted Git),
# ref resolution can favour the branch over the raw commit object when
# the two are ambiguous. `git checkout $SHA` then lands on attacker
# code while the reported pin still looks perfectly intact.

3. Why It Is Zero-Click: It Reaches People Who Did Everything Right

The full chain is what researchers have started calling a rug-pull. An attacker publishes a genuinely benign plugin that passes marketplace review and is pinned to its initial commit. Once it has an installed base, the attacker ships a routine-looking update, and the marketplace re-pins installations to the new commit hash. The attacker then creates a branch named after that hash, or FETCH_HEAD for Gemini CLI, pointing at malicious code. The next time an installed agent's background auto-update re-checks the plugin, the substituted code executes with zero user interaction and no error surfaced. That last point matters: because plugin auto-update is enabled by default in Claude Code and Codex, the chain needs nothing from the user. It also bypasses the discipline of installing only reviewed plugins, since the victim's behavior matched exactly what the security model asked for. Air has validated both ends of the chain in earlier research: a plugin it built spread to more than 26,000 agents before being pulled, and in research it calls SkillJacking, 925 skills already in active use had been hijacked from their original maintainers, reaching 134,000 agents.

# The durable fix: assert the RESOLVED state after checkout,
# not the reference you requested. Abort the moment they disagree.
set -euo pipefail

git clone --no-checkout "$URL" plugin && cd plugin
git fetch --depth 1 origin "$PINNED_SHA"
git checkout --detach "$PINNED_SHA"

RESOLVED=$(git rev-parse HEAD)
if [ "$RESOLVED" != "$PINNED_SHA" ]; then
  echo "pin bypass detected: asked for $PINNED_SHA, got $RESOLVED" >&2
  exit 1
fi

# A pin is only meaningful if the tree is also clean.
[ -z "$(git status --porcelain)" ] || { echo "dirty working tree" >&2; exit 1; }
echo "verified: $RESOLVED"

4. Why Marketplaces Cannot Fix This Alone

The natural instinct is to make the marketplace fix it. But the check runs inside the agent, not at the marketplace, so no marketplace can guarantee the protection on its own. One partial mitigation is to restrict where plugins may be hosted. GitHub rejects a 40-hex branch name outright, but Bitbucket and any self-hosted Git server allow it, and the affected agents all officially support those hosts. Confining plugin sourcing to platforms that reject SHA-shaped branch names blunts most of the first variant, but it does nothing for Gemini CLI's variant and it excludes self-hosted Git users. Air's conclusion is that the fix has to be enforced by the agent itself, immediately after checkout, by asserting that the resolved HEAD, not the reference it originally requested, matches the pinned SHA exactly, and aborting installation if it does not.

// Inventory what each installed plugin ACTUALLY resolves to, weekly,
// independently of what the agent self-reports.
const dirs = ["~/.claude/plugins/*", "~/.codex/plugins/*", "~/.copilot/plugins/*"];

for (const glob of dirs) {
  for (const d of await expand(glob)) {
    if (!(await isRepo(d))) continue;
    const head = await git(d, "rev-parse HEAD");
    const dirty = await git(d, "status --porcelain");
    console.log(d, "resolved=" + head, "dirty_files=" + dirty.split("\n").filter(Boolean).length);
  }
}
// Diff this list against the marketplace pin list. Any mismatch is an
// incident, not a curiosity: it means reviewed code is not what runs.

5. Five Things You Can Do Now

First, take inventory. List every coding agent and version across developer workstations and CI environments, and confirm it sits above the patched floor: Claude Code 2.1.179 or later, Codex 0.146.0 or later. Copilot and Gemini CLI currently have no vendor fix to verify against. Second, wherever you cannot confirm a patch, turn off plugin auto-update, which removes the zero-click element of the attack and forces any substitution to require a manual re-installation a vigilant user or endpoint control could catch. Third, treat every installed plugin as a software supply chain dependency: record the commit each one currently resolves to and re-verify that resolution independently of the agent's own self-reported pin status. Fourth, add a source gate that refuses plugin hosts allowing SHA-shaped branch names. Fifth, do the cheap high-value thing and run plugins inside a least-privilege sandbox, so that even a successful pin bypass yields a narrow, monitored blast radius rather than the full reach of the developer's credentials.

# Two cheap policy gates that close the widest path first.
# 1) Only install plugins from hosts that reject SHA-shaped branch names.
if git ls-remote --heads "$URL" | awk '{print $2}' | grep -qE 'refs/heads/[0-9a-f]{40}$'; then
  echo "host allows SHA-named branches - refusing this plugin source" >&2
  exit 1
fi

# 2) Where you cannot patch yet, turn OFF background auto-update.
#    Patched floors: Claude Code 2.1.179+, Codex 0.146.0+.
#    GitHub Copilot had no vendor fix at disclosure and Gemini CLI was
#    deprecated rather than patched, so existing installs stay exposed.

6. The Real Lesson: Verify the Resolved State, Not the Identifier You Asked For

Placed in a wider frame, Plugin4Shell reveals a recurring pattern that keeps moving one layer up the stack. The Cloud Security Alliance research note groups it with two related cases. GuardFall found that ten of eleven popular open-source coding agents could be tricked into executing arbitrary shell commands despite command-safety guardrails, because the guardrails inspected commands before the shell's own expansion and resolution logic ran. Plugin4Shell is the identical structural mistake one layer higher: the pinning control inspects and records the requested reference, then never checks that the request and the result actually agree. The conclusion holds for anyone building an agent, a marketplace, or a package manager. For a hash pin to function as an integrity control, it has to assert the state after resolution, not merely record which identifier you requested. When the promise of installing only reviewed code can be silently defeated, what remains is least-privilege sandboxing and independent verification, which is exactly the lesson a decade of supply chain security taught humans. Agents are simply being made to learn it too.

Plugin repositories and commit hashes

A branch name can look like a commit hash, and that is the whole door

Least-privilege sandboxing

The sandbox decides how far a single bypass travels

📌 Frequently Asked Questions

What is Plugin4Shell?

It is a zero-click remote code execution class disclosed by Air Security on September 18, 2026, defeating SHA pinning: four major coding agents check out a pinned commit without verifying the working tree landed there, so an attacker can substitute reviewed plugin code while the pin still looks intact.

Which agents are affected, and are they patched?

Claude Code, OpenAI Codex, GitHub Copilot, and Google Gemini CLI. Anthropic patched Claude Code in 2.1.179 and OpenAI patched Codex in 0.146.0; at disclosure Microsoft had no Copilot fix, and Google deprecated Gemini CLI in favor of Antigravity instead of patching, leaving existing installs exposed.

Why is it described as zero-click?

Because plugin auto-update is enabled by default in Claude Code and Codex. When the marketplace bumps the pinned hash, an installed plugin re-runs the same git checkout in the background and the swapped code executes with no user action and no error shown.

What should I do first?

Inventory and upgrade to a patched version where one exists (Claude Code 2.1.179+, Codex 0.146.0+), disable plugin auto-update where you cannot confirm a patch, record and independently re-verify the resolved commit of every installed plugin, restrict plugin hosting, and sandbox plugins with least privilege.

Why can't marketplaces fix this themselves?

Because the verification runs inside the agent rather than at the marketplace, so no marketplace can guarantee protection alone. Restricting hosts only blunts the first variant, does nothing for Gemini CLI's FETCH_HEAD variant, and excludes self-hosted Git users.