{"slug": "your-agent-pinned-a-plugin-to-a-hash-it-got-the-branch", "title": "Your Agent Pinned a Plugin to a Hash. It Got the Branch.", "summary": "AIR Security researchers Or Nevo, Dor Granat and Niv Hoffman published a September 17, 2026 advisory detailing Plugin4Shell, a flaw in which four coding agents pinned plugins to a commit hash but checked out an attacker-created branch of the same name instead, enabling zero-click remote code execution. Claude Code 2.1.179 and Codex 0.146.0 shipped fixes months before disclosure, while Copilot had no fix at disclosure and Google is not patching the retired Gemini CLI, according to the advisory. The advisory cites no CVE identifier and reports no exploitation in the wild.", "body_md": "A pin is a promise: install exactly this commit, nothing else. On September 17, 2026 three researchers at AIR Security published an advisory showing that four coding agents broke that promise. Each pinned a plugin to a commit hash and none checked that the commit it pinned was the one that landed; in three of the four, a repository owner who created a branch named after the hash got the branch checked out instead. The owner could then push whatever they liked to the branch, and every agent that auto-updated its plugins would install it without a prompt.\n\nTwo of the four had fixed it before the advisory went public. The rest of this post is about the part that outlives the patch: what git actually does when a name could be either a branch or a commit, how each extension system you rely on verifies the code it installs, and five questions to ask about your own setup. Every claim about the flaw is the researchers'; every claim about git is from git's own documentation.\n\n1. 01git prefers a branch over a commit when a name could be both.git's checkout documentation says a name that is a valid ref under refs/heads/ is treated as a branch, and only otherwise as a commit. An attacker who names a branch after a hash gets the branch.\n2. 02Claude Code 2.1.179 and Codex 0.146.0 carry the fix.Both shipped months before the September 17 disclosure. Copilot had no fix at disclosure, and Google is not patching the retired Gemini CLI, according to the advisory.\n3. 03No CVE and no exploitation in the wild are recorded.The advisory does not cite a CVE identifier or an observed attack. 'Zero-click' is the researchers' severity term, not evidence of use.\n4. 04The question to ask any extension system is what it checks after download.A hash in a manifest proves nothing unless the installer compares it with what arrived. Signed packages, provenance attestations and a checked-out commit compared against the pin are three different answers.\n\n## 01 — The advisoryWhat the researchers found\n\nThe [advisory](https://air.security/blog-posts/plugin4shell), by Or Nevo, Dor Granat and Niv Hoffman, dates the discovery to May 2026, coordinated disclosure to the vendors to June, and publication to September 17. The name they gave the class is Plugin4Shell. The mechanism as they describe it is short: a plugin marketplace records a commit hash, the agent runs a checkout of that hash, and if the repository has a branch whose name is the same string, git checks out the branch instead. In the researchers' words, \"when a name is both a valid ref and an object id, git prefers the ref…\"         \n\nThree of the four agents shared that variant. Gemini CLI's was different: a repository whose default branch is named FETCH_HEAD overrides the commit the agent had just fetched. The researchers classify the result as a zero-click remote code execution, meaning in their description that no user interaction is needed once the agent updates. That is their severity call and we report it as such. The advisory does not cite a CVE and does not report any exploitation in the wild, and we found none elsewhere.\n\nThe four products are the instance. The class is any installer that accepts a git reference as a pin and hands it to a checkout without checking what came back. That covers plugin marketplaces, skill packs, MCP server installers and home-grown deploy scripts alike, which is why the second half of this post is a census of verification rather than a story about four vendors.\n\n## 02 — The mechanismHow git decides: the *transferable* part\n\nNone of this is a git bug. It is documented behaviour. The [git checkout reference](https://git-scm.com/docs/git-checkout) describes the branch argument as follows: if the name, when prefixed with refs/heads/, is a valid ref, then that branch is checked out; otherwise, if it names a valid commit, HEAD becomes detached at that commit. A forty-character hexadecimal string is a perfectly legal branch name. So the moment such a branch exists, the first rule fires and the second is never reached.\n\ngit does try to warn. The [configuration reference](https://git-scm.com/docs/git-config) documents a setting, on by default, under which git will warn you when a ref name you passed is ambiguous and might match several refs. A warning on standard error is exactly the kind of signal an automated installer discards. The fix is the same in every product: after the checkout, read the commit that HEAD points at and refuse if it differs from the pin, or fetch the object by hash directly and never pass the string through name resolution at all.\n\nIf true, Git will warn you if the ref name you passed it is ambiguous and might match multiple refs in the repository. True by default.git-config documentation, core.warnAmbiguousRefs, read September 22, 2026\n\n## 03 — The fourThe four agents and their patch state\n\nPatch states below are those the advisory records, cross-checked against each vendor's own release record where one exists. The Claude Code version was dated from the npm registry. The Codex version was dated from its [GitHub release](https://github.com/openai/codex/releases/tag/rust-v0.146.0). Neither vendor's notes name the advisory.\n\n| AIR Security advisory of September 17, 2026; npm registry; the Codex release page; Google's May 19, 2026 retirement post. Patch states as of the advisory, not re-verified after September 20. |  |  |  | \n|---|---|---|---|\n| Agent | State at disclosure | Version and date | What a user does | \n|---|---|---|---|\n| Claude Code | Patched before disclosure | 2.1.179, published to npm June 16, 2026; the researchers confirmed the fix June 17. The public changelog entry for that version does not mention it. | Run the CLI with its version flag; update if below 2.1.179. | \n| OpenAI Codex | Patched before disclosure | 0.146.0, released July 29, 2026, whose notes include the line 'Verify Git plugin SHA checkouts'; the researchers verified it August 12. | Run the CLI with its version flag; update if below 0.146.0. | \n| GitHub Copilot | No fix shipped at disclosure | The advisory records no patched version. We read no GitHub statement dated on or before September 20. | Treat every plugin pin as a branch reference until GitHub documents a fix. | \n| Gemini CLI | No patch planned | The advisory's timeline records Google confirming on August 4, 2026 that no fix will ship because the Gemini CLI is deprecated, and advising migration to Antigravity. The variant differs: a default branch named FETCH_HEAD overrides the fetched commit. | Google's own May 19 announcement points users to Antigravity; Code Assist licence holders keep access. | \n\nOne detail from Claude Code's current documentation is worth knowing if you write a marketplace. Its [plugin marketplace reference](https://code.claude.com/docs/en/plugin-marketplaces) says that when a git-based plugin source sets both a ref and a sha, the sha is the effective pin and Claude Code fetches and checks out the pinned commit directly. The marketplace catalog itself, by contrast, supports a ref but not a sha, so the list of plugins can still move even when each plugin cannot. Version 2.1.277, released September 18, also fixed the installed-plugins record dropping the commit for official-marketplace plugins, which is the record a user would read to know what they actually have.\n\n## 04 — The censusWhat other extension systems verify\n\nAgents install code from more places than plugin marketplaces. Each system below is classified by what its own documentation says the installer checks after download. One row is marked unverified, because we read no first-party statement in this pass for either system it names; they are named so that you check them, not skipped so that you forget.\n\n##### VS Code extensions\n\nThe Visual Studio Marketplace signs every extension at publish time and VS Code verifies that signature on install to check integrity and source. Since release 1.97 a first install from a third-party publisher also asks you to confirm you trust the publisher.\n\n##### npm packages\n\nnpm's provenance statements record where a package was built and who published it, verifiable by consumers. Since September 18, a stage-only token can submit a version that a maintainer must approve with two-factor authentication before it publishes.\n\n##### Claude Code plugin sources\n\nA git source with a sha is fetched and checked out at that commit; an archive source may carry a SHA-256 digest. The marketplace catalog itself pins by ref only.\n\n##### MCP servers and skill packs\n\nMost MCP servers are started from a package name or a git URL by whatever runner the config names, and skill packs are folders copied from a repository. We read no first-party verification statement for either; treat both as unpinned until you have one.\n\nThe pattern across the verified rows is that the check happens on the artifact, not the name: a signature over the package, an attestation over the build, a commit compared against the pin. The unverified row is the one where a name is all there is. Our earlier posts on [skill packs as a package ecosystem](https://www.digitalapplied.com/blog/agent-skill-packs-package-ecosystem-supply-chain-risk) and on [the lessons of an earlier plugin incident](https://www.digitalapplied.com/blog/ai-agent-plugin-security-lessons-clawhavoc-2026) cover those two categories in depth.\n\n## 05 — The checkFive questions for your own setup\n\nYou do not need to reproduce anything to know where you stand. Five questions, each answerable from a version number or a configuration file, cover the class.\n\n- What version am I on?Claude Code at or above 2.1.179; Codex at or above 0.146.0\n- Version flag\n- Does my agent update plugins without asking?Auto-update is the step that turns a bad branch into an install\n- Settings\n- Are my plugins pinned by full commit hash or by ref?A tag or branch name is a pointer the owner controls\n- Manifest\n- Does the installer compare what it checked out with the pin?The fix in both patched agents; ask your vendor if undocumented\n- Vendor docs\n- Can I read which commit is actually installed?Claude Code records it in installed_plugins.json; 2.1.277 fixed missing and stale commits there\n- Local record\n\nThe credential side of the same question, what a token lets an automated publisher do, is the subject of our companion post on [stage-only npm tokens](https://www.digitalapplied.com/blog/ci-agent-npm-publishing-tokens-stage-only). And what still does not port when you move plugins between agents is in [our plugins portability post](https://www.digitalapplied.com/blog/agent-plugins-1-0-ga-what-still-does-not-port). If you run agents that install their own extensions, our [AI transformation service](https://www.digitalapplied.com/services/ai-transformation) includes this check as part of an agent security review.\n\n## 06 — Next stepA pin is only as good as the check after the download\n\n### Run the five questions against every agent that installs code for you\n\nUpdate Claude Code and Codex if you are below the fixed versions. Stop letting Copilot pull plugin updates unattended until GitHub documents a fix, and move off the retired Gemini CLI. Then open every manifest that pins a git source and replace any ref with a full commit hash, knowing that even that is only a promise until the installer checks it.", "url": "https://wpnews.pro/news/your-agent-pinned-a-plugin-to-a-hash-it-got-the-branch", "canonical_source": "https://www.digitalapplied.com/blog/agent-plugin-sha-pinning-what-your-tool-verifies", "published_at": "2026-09-20 00:00:00+00:00", "updated_at": "2026-09-22 12:25:52.998789+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-tools", "developer-tools"], "entities": ["AIR Security", "Or Nevo", "Dor Granat", "Niv Hoffman", "Claude Code", "Codex", "GitHub Copilot", "Gemini CLI"], "alternates": {"html": "https://wpnews.pro/news/your-agent-pinned-a-plugin-to-a-hash-it-got-the-branch", "markdown": "https://wpnews.pro/news/your-agent-pinned-a-plugin-to-a-hash-it-got-the-branch.md", "text": "https://wpnews.pro/news/your-agent-pinned-a-plugin-to-a-hash-it-got-the-branch.txt", "jsonld": "https://wpnews.pro/news/your-agent-pinned-a-plugin-to-a-hash-it-got-the-branch.jsonld"}}