Plugin4Shell: Patch and Harden Your Coding Agent (2026)
September 21, 2026
Plugin4Shell is a zero-click bug in how Claude Code, OpenAI Codex, GitHub Copilot CLI, and the Gemini CLI install plugins: all four check out a plugin at a pinned commit SHA but never verify the checkout landed there, letting a repository owner serve different code under an intact-looking pin1. Claude Code and Codex have shipped fixes; Copilot CLI and Gemini CLI have not. This post covers what changed, what didn't, and the exact settings that reduce your exposure on all four.
TL;DR
Security researchers at AIR disclosed Plugin4Shell on September 17, 2026: a SHA-pinning bypass affecting Claude Code, Codex, GitHub Copilot CLI, and the Gemini CLI, found in May and privately reported to all four vendors in June1. Anthropic patched Claude Code in version 2.1.179 (released June 16, fix confirmed June 17)12. OpenAI patched Codex in version 0.146.0 (released July 29, fix verified August 12), and the fix is a named commit in the public changelog: "Verify Git plugin SHA checkouts"3. Microsoft has not shipped a client-side fix for Copilot CLI, though GitHub separately argues its own hosting already blocks the main attack path; Google has declined to patch the deprecated Gemini CLI at all and is pointing users to Antigravity instead14. No CVE had been assigned as of this writing5. The bug requires no user interaction because it rides the same background auto-update that keeps plugins current — the update path, not the install path, is what makes it zero-click1.
What you'll learn
- What Plugin4Shell actually breaks, without a working exploit — the missing verification step, not the pinning mechanism itself
- Which agent versions are patched, which aren't, and how to check your installed version right now
- Why GitHub and GitLab structurally block one variant of the bug and Bitbucket and self-hosted git servers don't
- How to lock Claude Code to an allowlist of trusted plugin marketplaces with
strictKnownMarketplaces - How to pin GitHub Copilot CLI plugins by commit SHA and turn off unattended plugin auto-update
- A verification script that checks your installed agent versions against the patched thresholds
What happened, and when
AIR's research team — Or Nevo, Dor Granat, and Niv Hoffman — found the bug in May 2026 with working proof-of-concept exploits against all four agents, and disclosed it to Anthropic, OpenAI, Google, and Microsoft under coordinated disclosure the following month1. That timeline matters: by the time AIR published its public write-up on September 17, Claude Code had already been carrying a silent fix for three months, and Codex for about seven weeks. If you've been running an auto-updated, current install of either agent since mid-summer, you were very likely already protected before this story broke1.
| Date | Event |
|---|---|
| May 2026 | AIR finds the bug, builds working PoCs against all four agents |
| June 2026 | Disclosed to Anthropic, OpenAI, Google, and Microsoft |
| 2026-06-17 | Anthropic confirms the fix, shipped in Claude Code 2.1.179 |
| 2026-08-04 | Google confirms no fix will ship; Gemini CLI is deprecated |
| 2026-08-12 | AIR verifies Codex 0.146.0 as fixed |
| 2026-09-17 | AIR publishes the public disclosure; GitHub Copilot CLI still has no fix |
Source: AIR's disclosure timeline1.
How the bypass works, at the level that matters for defense
All four agents let you extend them with plugins pulled from a marketplace — a git repository containing a manifest that lists available plugins and, for each one, a commit SHA the marketplace maintainer reviewed and pinned. The promise of pinning is that once a plugin passes review, it can't change under you without someone noticing.
AIR's technical write-up states the core defect plainly: every agent "checks out the pinned commit but never checks that it actually landed there"1. Concretely, an attacker who controls the plugin's upstream repository — either by publishing a plugin that starts out genuinely benign and turning it malicious later, or by taking over the repository behind a plugin other people already trust — can create a ref on that repository that collides with the pin. For Claude Code, Codex, and GitHub Copilot CLI, that collision is a branch literally named after the 40-character pinned SHA, set as the repository's default branch. For the Gemini CLI, which pins with git fetch and then runs git checkout FETCH_HEAD, the collision is a default branch literally named FETCH_HEAD1. In both cases, the working tree an agent ends up running is not guaranteed to be the commit its manifest says it reviewed, and the agent reports a successful install at the pinned SHA regardless.
This site isn't going to walk through the exact git resolution steps that make the swap happen — that's the part of AIR's write-up that functions as exploit detail, and independent analysts have already flagged that even AIR's own phrasing of the git-internals mechanics is being debated in follow-on technical coverage6. What's uncontested across every source is the practical shape of it: the pin gets checked once at install, the same checkout logic re-runs on unattended background auto-update — the default in Claude Code and Codex — and nothing after that checkout compares the commit that actually landed against the SHA that was supposed to land15. That's what makes it zero-click: a plugin you already installed, from a marketplace you already trust, can be swapped out from under you with no prompt and nothing to notice1.
Who's patched
| Agent | Status | Patched version | Source |
|---|---|---|---|
| Claude Code | Patched | 2.1.179 (June 16, 2026) | Confirmed via GitHub release page2 |
| OpenAI Codex | Patched | 0.146.0 (July 29, 2026) | Fix is PR #34644, "Verify Git plugin SHA checkouts"3 |
| GitHub Copilot CLI | No client fix | — | GitHub says its own hosting mitigates the main variant4 |
| Gemini CLI | Will not be patched | — | Google has deprecated the product1 |
Check what you're actually running before trusting this table — patch numbers only help if you're past them:
claude --version # need 2.1.179 or later
codex --version # need 0.146.0 (rust-v0.146.0) or later
copilot --version # no patched version exists yet — see mitigations below
A small script to check all three at once and flag anything below the patched threshold:
#!/usr/bin/env bash
# plugin4shell-version-check.sh — flags coding agents below Plugin4Shell's patched versions.
set -euo pipefail
version_lt() { [ "$1" != "$(printf '%s\n%s' "$1" "$2" | sort -V | tail -n1)" ]; }
check() {
local name="$1" cmd="$2" min="$3" ver
if ! command -v "$cmd" >/dev/null 2>&1; then
echo " $name: not installed, skipping"
return
fi
ver=$("$cmd" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1 || echo "unknown")
if [ "$ver" = "unknown" ]; then
echo " $name: could not parse version from '$cmd --version'"
elif version_lt "$ver" "$min"; then
echo " $name: $ver — BELOW patched version $min, update now"
else
echo " $name: $ver — at or above patched version $min"
fi
}
echo "Plugin4Shell version check ($(date -u +%Y-%m-%d)):"
check "Claude Code" claude 2.1.179
check "Codex" codex 0.146.0
echo " Copilot CLI: no patched version exists as of 2026-09 — pin plugins by SHA and disable auto-update (see below)"
echo " Gemini CLI: will not be patched — plan a migration to Antigravity"
This only checks version numbers. It doesn't touch your marketplaces, doesn't check any plugin's actual checked-out commit, and makes no network calls — it's safe to run as-is.
The host mitigation that may already cover you — and the one that doesn't
Because the check that's missing runs inside the agent, no marketplace can fully close the gap on its own — but one variant of the bug depends on being able to name a branch after a 40-character hex string, and two major git hosts refuse to let you do that. GitHub's own documentation states it directly: it restricts branch and tag names "which look like Git object IDs (40 characters containing only 0-9 and A-F)," specifically to prevent confusion with actual commit references7. GitLab's branch documentation carries the identical rule — "branch names with 40 hexadecimal characters are prohibited"8 — because they resemble Git commit hashes, enforced as a push-side check. Independent coverage published after AIR's own disclosure has pointed out that GitLab gets grouped with the unprotected hosts in some early write-ups, which is incorrect — the same protection exists on both6.
That leaves two categories exposed to the branch-name variant: Bitbucket, which AIR confirmed permits SHA-shaped branch names, and self-hosted git servers — anything from a bare repo behind SSH to Bitbucket Data Center or Gitea — which permit it unless someone has specifically configured a rejection rule16. Anthropic's own documentation explicitly lists Bitbucket and self-hosted git as supported marketplace backends1. GitHub's Copilot CLI marketplace documentation is less specific — it describes marketplaces as installable from GitHub.com, "any other online Git hosting service," or a local file system, without naming Bitbucket or self-hosted git directly, though the generic language covers them the same way6. Either way, that's precisely why an enterprise-run internal marketplace is the realistic exposure, not the public catalogs. And the host-level mitigation does nothing at all for the Gemini CLI's separate FETCH_HEAD variant, which doesn't depend on a hash-shaped name in the first place1.
The practical takeaway: if every plugin marketplace your team has added points at github.com or gitlab.com, you're not exposed to the branch-name variant regardless of which agent version you run — but you should still update, because that host-side protection isn't something your agent configuration controls, and it moves the moment a marketplace does.
Audit your marketplaces before you trust any of them
Do this before changing any settings, since it tells you whether you have anything to fix beyond updating:
- List every marketplace you've registered. For Claude Code, check
extraKnownMarketplacesin every managed, project, and user settings file you have. For Copilot CLI, runcopilot plugin marketplace list9. - Sort the results by git host. Anything on github.com or gitlab.com is covered by the host-side block described above. Anything else — an internal GitLab-alternative, a bare repo, Bitbucket — is your actual exposed set. If you have one, it's likely the internal marketplace nobody thought to flag as a security boundary, precisely because it was never on a public catalog.
- Spot-check one pin by hand. In an installed plugin's local checkout, run
git rev-parse HEADand compare the output to the SHA declared in the marketplace's manifest. This is the exact comparison the agents skip — doing it once tells you whether the pin in your environment was ever actually enforced.
Lock Claude Code to an allowlist with strictKnownMarketplaces
Claude Code supports a managed-settings allowlist for plugin marketplaces, and its enforcement point lines up with this bug: the check runs "before any network or filesystem operation," on marketplace add and on every plugin install, update, refresh, and auto-update10. An organization can restrict marketplaces to specific repositories, entire GitHub owners, or any host matching a regex:
{
"strictKnownMarketplaces": [
{ "source": "github", "repo": "anthropics/claude-plugins-official" },
{ "source": "github", "repo": "acme-corp/*" },
{ "source": "hostPattern", "hostPattern": "^gitlab\\.example\\.com$" }
]
}
The github source with an owner/* wildcard matches every repository under that owner; hostPattern matches by regex against the marketplace's host, which is the entry to use if your organization's git host isn't GitHub — useful specifically because it lets you allowlist "our GitLab instance" without listing every repo on it10. The inverse control, blockedMarketplaces, supports the same owner/* wildcard as of Claude Code v2.1.22310. Setting strictKnownMarketplaces to an empty array blocks every marketplace, including Anthropic's own — useful as a starting point you then open up deliberately, not as a permanent setting.
Because this lives in managed settings, individual developers and project-level configuration can't override it10. Pair it with extraKnownMarketplaces in the same file to pre-register the marketplaces the allowlist permits, since the allowlist alone doesn't add anything for users automatically.
Background auto-update itself defaults differently depending on the marketplace's origin: Anthropic's official marketplace, most other Anthropic-run catalogs, and marketplaces added from claude.ai have it on by default, while third-party and local development marketplaces default to auto-update off — until a user turns it on for that marketplace, or an administrator sets "autoUpdate": true on its extraKnownMarketplaces entry in managed settings11. That default cuts in your favor for an internal marketplace nobody has opted into auto-update for; it stops cutting in your favor the moment someone flips that switch for convenience, which is exactly what the setting exists to let them do.
Pin and lock down GitHub Copilot CLI
Copilot CLI has no version-level fix yet, so the available controls are pinning and update behavior rather than a version bump. Both github and url-sourced plugin entries accept a sha field, documented directly as the way to get "reproducible installs that are immune to force-pushes or tag/branch moves" — exactly the guarantee Plugin4Shell defeats when it isn't paired with a host-side block9:
{
"source": {
"source": "github",
"repo": "owner/repo",
"sha": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3",
"path": "plugins/my-plugin"
}
}
For auto-update: first-party plugins from Copilot's built-in marketplaces update automatically at the start of each session in a trusted working directory, and any custom marketplace you've added can opt into that same behavior by setting autoUpdate: true on its extraKnownMarketplaces entry — which also means it's opt-out, not opt-in, for anything you've deliberately enabled. Set autoUpdate: false in your Copilot CLI configuration, or export COPILOT_AUTO_UPDATE=false, to turn an unattended swap back into a command someone has to run and notice.
If you're on the Gemini CLI
Google's position is unambiguous on the fix: none is coming, because the standalone Gemini CLI is deprecated in favor of Antigravity1. What Google's own transition notice does not say is anything about security maintenance for the accounts it describes as still supported — organizations on a Gemini Code Assist Standard or Enterprise license keep CLI access and continue receiving model updates past the individual-user cutoff, with no separate statement about whether that includes a fix for this specific bug. If your team holds that license, the open question worth putting to your account team in writing is whether "supported" extends to this vulnerability specifically, not just to model access. Migrating to Antigravity is the vendor-recommended path and it does remove this specific bug's attack surface, since Antigravity has no marketplace plugin SHA-pinning mechanism to bypass1 — but treat it as a genuine migration with its own review, not a same-day swap.
Defense in depth: constrain what a plugin can reach
Pinning and allowlisting reduce which code runs; they don't limit what that code can do once it's running. Claude Code's sandbox supports a network.allowedDomains allowlist that restricts outbound connections regardless of which plugin issued them, and Codex's sandbox_workspace_write.network_access setting is disabled by default, requiring an explicit opt-in before a sandboxed command gets any network access at all. Neither setting is specific to this bug, and neither is a substitute for the marketplace-level controls above — but both mean that even a plugin that did get swapped has less to reach.
Verification
Every version number and configuration field in this post was checked against a primary source rather than secondhand summaries. Claude Code's patched version and release date came from Anthropic's own GitHub release page, fetched directly2. Codex's patched version, release date, and the specific fixing commit came from OpenAI's own GitHub release page for rust-v0.146.0, which lists "Verify Git plugin SHA checkouts" by name in its changelog3. The GitLab branch-naming restriction was confirmed by fetching GitLab's own documentation page directly, not by trusting secondary reporting of it8. Claude Code's strictKnownMarketplaces syntax, enforcement points, and the owner/* wildcard's version requirement were confirmed by fetching Anthropic's plugin-marketplaces documentation directly and reading the configuration examples in full10. GitHub Copilot CLI's sha field, its autoUpdate/COPILOT_AUTO_UPDATE behavior, and the copilot plugin marketplace list command were confirmed against GitHub's own CLI reference docs9. Claude Code's marketplace-dependent auto-update defaults were confirmed directly against Anthropic's own documentation11. This post was revised on 2026-09-22 after a second verification pass: one footnote had cited the wrong source for the no-CVE-assigned claim (corrected to the source that actually states it), and the auto-update-defaults paragraph above was added after direct source review turned up that nuance. No exploit or proof-of-concept was built, run, or is included anywhere in this post — the version-check script above only reads locally installed version strings and makes no network requests or git operations.
Bottom line
Plugin4Shell is a missing assertion, not a broken cryptographic primitive: every affected agent checks out a commit and trusts that the working tree matches it, without ever confirming that afterward. Two vendors closed that gap in their own code months before the public disclosure landed; two have not, for different reasons — one citing a host-side mitigation that only covers part of the bug, one because the product itself is being retired. None of that changes what you can control this week: check your installed versions against the table above, inventory which git hosts your plugin marketplaces actually live on, and turn the allowlisting and pinning controls this post walks through from documented-but-unused into configured. The pin was only ever as strong as the thing verifying it, and for months, the thing verifying it was a naming rule on someone else's git host.
Related reads
- AI Agent Visibility Gap: Snyk's 2026 Numbers, Checked
- AI Agent Containment: What Four 2026 Incidents Show
- Claude Managed Agents: ant apply Tutorial (2026)
Footnotes
-
AIR Security, "Plugin4Shell – Zero Click RCE Vulnerability found in top 4 most popular coding agents" — https://www.air.security/blog-posts/plugin4shell (by Or Nevo, Dor Granat, Niv Hoffman; published 2026-09-17; primary disclosure including technical mechanism, affected agents, mitigation status, and full timeline table; fetched 2026-09-21) ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19 ↩20 ↩21
-
GitHub, "Release v2.1.179 · anthropics/claude-code" — https://github.com/anthropics/claude-code/releases/tag/v2.1.179 (released 2026-06-16 by ashwin-ant; primary source for Claude Code's patched version and release date; fetched 2026-09-21) ↩ ↩2 ↩3 ↩4
-
GitHub, "Release 0.146.0 · openai/codex" — https://github.com/openai/codex/releases/tag/rust-v0.146.0 (released 2026-07-29; changelog lists PR #34644, "Verify Git plugin SHA checkouts"; primary source for Codex's patched version, release date, and fixing commit; fetched 2026-09-21) ↩ ↩2 ↩3 ↩4
-
InfoWorld, "A zero-click RCE flaw in AI coding agents could have exposed enterprise systems" — https://www.infoworld.com/article/4223907/a-zero-click-rce-flaw-in-ai-coding-agents-could-have-exposed-enterprise-systems.html (published 2026-09-18, Anirban Ghoshal; reports GitHub's statement to The Register that its branch-naming restriction blocks the reported vulnerability on GitHub; fetched 2026-09-21) ↩ ↩2
-
The Hacker News, "Plugin4Shell Lets Repository Owners Swap Pinned Plugin Code Across Four AI Coding Agents" — https://thehackernews.com/2026/09/plugin4shell-lets-repository-owners.html (published 2026-09-18, Swati Khandelwal; independent verification that no CVE identifier had been assigned and no vendor had published a security advisory as of September 18; fetched 2026-09-22) ↩ ↩2 ↩3
-
THE DAILY BRIEF (beri.net), "The Pinned Commit Was a Branch. Check Your Git Host." — https://www.beri.net/article/plugin4shell-sha-pin-bypass-coding-agent-plugin-git-host-inventory (by Rajesh Beri, published 2026-09-20; independent follow-on analysis correcting the GitLab-as-unprotected claim in earlier coverage and providing the host-inventory framing; fetched 2026-09-21) ↩ ↩2 ↩3 ↩4
-
GitHub Docs, "Dealing with special characters in branch and tag names" — https://docs.github.com/en/get-started/using-git/dealing-with-special-characters-in-branch-and-tag-names (states GitHub restricts branch/tag names "which look like Git object IDs (40 characters containing only 0-9 and A-F)"; fetched directly 2026-09-22, confirmed against the doc's raw source) ↩ ↩2
-
GitLab Docs, "Branches" — https://docs.gitlab.com/user/project/repository/branches/ ("Branch names with 40 hexadecimal characters are prohibited, because they are similar to Git commit hashes"; fetched directly 2026-09-21) ↩ ↩2 ↩3
-
GitHub Docs, "GitHub Copilot CLI plugin reference" — https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-plugin-reference (documents the
shapinning field,copilot plugin marketplace listcommand, and--jsonoutput shape; fetched directly 2026-09-21) ↩ ↩2 ↩3 -
Claude Code Docs, "Plugin marketplaces" — https://code.claude.com/docs/en/plugin-marketplaces (documents
strictKnownMarketplaces,blockedMarketplaces, theowner/*wildcard's v2.1.223 requirement,hostPattern/pathPatternsources, and the background auto-update mechanism; fetched directly 2026-09-21) ↩ ↩2 ↩3 ↩4 ↩5 -
Claude Code Docs, "Discover and install prebuilt plugins through marketplaces" — https://code.claude.com/docs/en/discover-plugins (documents that the official Anthropic marketplace, most other Anthropic-run catalogs, and marketplaces added from claude.ai have auto-update enabled by default, while third-party and local development marketplaces default to disabled, and that administrators can override this per marketplace via
autoUpdate: trueon anextraKnownMarketplacesentry; fetched directly 2026-09-22) ↩ ↩2


