CSIPE

Published

- 18 min read

A Pinned Coding-Agent Plugin Still Needs a Receipt


Books by the author

Compare all 5

As an Amazon Associate I earn from qualifying purchases. Buying through these links costs you nothing extra and helps pay for the blog.

A team reviews a coding-agent plugin, records the exact Git commit, and lets the agent install it. That sounds like the careful path. The commit is a long, specific identifier rather than a moving branch such as main, so the team expects the reviewed code to be the code on disk.

On 17 September 2026, Air Security described a flaw that broke that expectation across Claude Code, OpenAI Codex, GitHub Copilot, and Gemini CLI. The researchers called it Plugin4Shell. Their central finding was narrow and serious: an affected installer could ask Git for a pinned commit but fail to check whether the resulting working tree actually landed on that commit. A repository owner could exploit Git’s name resolution and supply different code while the installer still appeared to honor the pin (Air Security: Plugin4Shell).

There is important restraint to preserve here. As of 18 September, no CVE had been assigned, none of the four vendors had published a security advisory for the flaw, and there was no evidence that attackers had used it in the wild. The default catalogs checked by The Hacker News also pointed to GitHub, which blocks the hash-shaped branch name needed for the main variant (The Hacker News: Plugin4Shell lets repository owners swap pinned plugin code). This was a demonstrated supply-chain failure, not a reason to declare every plugin compromised.

The practical lesson reaches beyond these four products. A requested identity and an observed result are two different facts. If your control records only what the installer asked for, it can issue a clean receipt for the wrong parcel.

What happened, and what did not

Air Security said it found the issue in May 2026, built working demonstrations against all four coding agents, and disclosed the finding to the vendors in June. Anthropic later fixed Claude Code in version 2.1.179, according to Air. OpenAI fixed Codex in version 0.146.0. Air reported no available GitHub Copilot fix and said Google would not repair the retiring Gemini CLI (Air Security: Plugin4Shell).

OpenAI’s public change gives the strongest vendor-side confirmation of the mechanism. Its fix says Git can interpret a requested commit SHA as a branch name, causing a plugin source to “materialize a different commit than the one it pinned.” The repaired installer verifies the resolved checkout rather than trusting the requested name (OpenAI Codex pull request 34644). Codex 0.146.0 contains that change (OpenAI Codex 0.146.0 release).

That evidence supports a precise claim: the installation path had an identity-checking bug. It does not support several broader claims that spread easily around an alarming headline. There is no public evidence, as of 19 September, that someone used Plugin4Shell against a real victim. There is no basis for assuming that every installed plugin changed. There is also no basis for treating every marketplace and every Git host as equally exposed.

The main Claude Code, Codex, and Copilot variant depends on a repository host accepting a branch whose name looks exactly like a 40-character commit identifier. Git itself permits such a name. GitHub rejects it, while Bitbucket and some self-hosted Git services may allow it, according to Air and the independent checks reported by The Hacker News. A plugin sourced only from a GitHub repository therefore avoids that branch-name route. Gemini CLI had a separate FETCH_HEAD name-confusion route, so the GitHub limitation does not settle its case (The Hacker News: Plugin4Shell lets repository owners swap pinned plugin code).

Background updates create the “zero-click” part of the story. Air said Claude Code and Codex could refresh an already installed plugin automatically. If a marketplace accepted a routine new pin while the repository owner prepared the conflicting name, the affected agent could replace the local plugin without a fresh install prompt. Yet the conditions matter: The Hacker News found that automatic updates were on by default for built-in catalogs hosted on GitHub, while outside catalogs used different defaults or required an explicit choice. The most dramatic path therefore combined controls that did not always coexist in a normal default setup.

That qualification makes the response better, not weaker. Teams need to identify their actual plugin sources, hosts, update settings, and agent versions. A generic warning cannot tell you whether your machines crossed the vulnerable path. Your own inventory can.

How a commit pin lost its meaning

A commit identifier is usually a strong way to name code. A branch can move from one commit to another; a full commit ID names one object. When a reviewer approves commit bbb...bbb, the useful promise is that the files executed later will come from that exact object.

The affected installers weakened the promise by mixing two layers. First, they passed the expected identifier to Git. Second, they assumed a successful command meant the working tree matched that identifier. Git’s rules leave room between those layers because a string can sometimes be interpreted as more than one kind of name.

For Claude Code, Codex, and Copilot, Air described a repository whose default branch was named with the same 40 hexadecimal characters as the pinned commit. A clone brought that default branch into the local repository. When the installer then asked Git to check out the apparent commit, Git preferred the matching reference and emitted an ambiguity warning. The working tree followed the branch, which the repository owner could point at different content, instead of the immutable commit the marketplace had reviewed (Air Security: Plugin4Shell).

Gemini CLI reached the same bad result by a different route. Its installer fetched the pinned object and then checked out FETCH_HEAD. Air found that a malicious repository could use FETCH_HEAD as its default branch name. The checkout could resolve the branch rather than the special file containing the fetched object’s identity. The correct object arrived, but the installer placed other files in the working tree.

No model reasoning was required. No prompt injection persuaded the coding agent to ignore a rule. The failure sat in ordinary package plumbing: clone, fetch, checkout, trust the exit code. That is why “the model would never do that” offers no protection. The model does not choose how an installer resolves a Git name.

The missing control fits in one comparison. After checkout, resolve the actual HEAD commit and compare it with the expected full identifier. Abort if they differ. Air presented that check as the common repair, and OpenAI’s public fix describes the same class of verification. The installer must inspect the state it created, not the label it requested.

Think of the pin as the delivery address printed on an order. The post-install comparison is the moment someone opens the box and checks the serial number. A purchase record proves what was ordered. It cannot prove what arrived.

This distinction appears all over engineering systems. A deployment request names an image digest, but the running workload has an observed image identity. An access policy says a role should be read-only, but an authorization test records what the role can do. A coding agent claims it entered a restricted workspace, but a probe shows which folders and network destinations remain reachable. Strong controls pair intent with observation.

Why plugins deserve production-grade treatment

A coding-agent plugin can look like a small convenience: a prompt, a skill, a command bundle, perhaps a Model Context Protocol server configuration. The security question is not how many lines it contains. The question is what happens when the agent loads it.

Air said malicious plugin code could run with the access of the user running the agent. On a typical developer machine, that may include source trees, package credentials, cloud command-line sessions, SSH configuration, browser tokens, signing tools, and access to internal repositories. The exact list differs by workstation, which is why a product headline cannot calculate your blast radius (Help Net Security: Zero-click RCE vulnerability hit four major AI coding agents).

A compromised plugin does not need to defeat every security control at once. Reading one package token can be enough to publish under a trusted name. Changing one build file can wait for a human to run the normal release job. Copying one internal configuration file can reveal service names and identity paths that make a later attack easier. The plugin inherits the useful routes already open to the developer.

That is the deeper reason the version check matters. Review teams often spend their effort on the content of a plugin and treat distribution as a solved problem. They inspect scripts, check requested tools, pin an approved commit, and record the decision. Plugin4Shell showed that the last metre between the pin and the local working tree also belongs inside the review.

The right response is not to ban every plugin. Plugins can package repeatable workflows, reduce improvised shell commands, and make a team’s preferred checks easier to use. The response is to stop granting them the casual trust reserved for text snippets. If an add-on can cause commands to run, read a repository, or open a tool connection, manage it like executable code.

That means ownership, an approved source, a version policy, a bounded runtime, and evidence of what was installed. It also means a removal path. Teams discover old agent extensions months after a trial ended because no one assigned an owner or expiry date. A plugin without a current owner becomes a small, quiet dependency with workstation access.

The Secure Harness makes the same point about coding agents more broadly: useful autonomy belongs inside externally enforced limits. A plugin is part of that harness only when its own installation and authority are controlled. Otherwise it quietly extends the agent’s reach while borrowing the marketplace’s reputation.

Work out whether your team was exposed

Start with inventory, not scanning every laptop for an imaginary payload. The public research gives you a set of conditions to test. Your job is to turn those conditions into a short exposure record for each agent installation.

First, record the product and the version actually running. Do not accept “we updated the package” as evidence. Open the agent, print its version from the same executable developers invoke, and capture the path to that executable. Shell aliases, multiple package managers, stale desktop integrations, and remote development containers can leave an older copy active after a successful update.

For Claude Code, Air identifies 2.1.179 as the repaired version. For Codex, OpenAI’s public release confirms that 0.146.0 includes the checkout verification. Those version statements are current as of 19 September 2026. Anthropic’s release notes did not publicly describe the security change when The Hacker News checked, so the Claude Code fix attribution still rests on Air’s coordinated-disclosure account rather than an Anthropic advisory (The Hacker News: Plugin4Shell lets repository owners swap pinned plugin code).

Second, list installed plugins and the marketplaces or direct repository URLs that supplied them. Capture the Git host for each source. A GitHub-hosted repository changes the assessment for the hash-shaped branch route because GitHub rejects that name form. A Bitbucket or self-hosted repository needs closer attention. Gemini CLI deserves separate treatment because its reported variant did not depend on a 40-character branch.

Third, record update behavior. Was the plugin installed once and left fixed? Does the agent update it in the background? Can an administrator force updates across the fleet? Can a developer add an outside marketplace without review? Automatic updates are not inherently bad, but they turn source integrity into a continuous requirement rather than an install-day check.

Fourth, identify the authority available during plugin loading and execution. Note whether the agent can read home-directory credentials, write outside the project, reach internal services, use a cloud session, call container or orchestration sockets, or access signing material. This is the consequence side of the assessment. A vulnerable checkout on a disposable, credential-free test machine is a different incident than the same checkout on a release engineer’s laptop.

Fifth, preserve evidence before cleaning up anything suspicious. Save the installed plugin directory, the local Git metadata when present, agent logs, update timestamps, source URL, expected pin, resolved HEAD, file hashes, and the version of the installer. If the expected and observed commits differ, disconnect the affected environment from sensitive systems and move into your incident process. Reinstalling immediately may remove the best evidence of how the files arrived.

Most teams will finish this check with one of three conclusions. Some installations were never on a vulnerable version. Some used the affected path but only with GitHub-hosted sources that blocked the main name-confusion condition. Others need inspection because the version, host, plugin source, or observed checkout leaves a credible route. Write down which conclusion applies and why.

A clean assessment should also state its limits. Updating an installer can stop a future bad checkout, but the public sources do not establish that the update finds or removes a plugin already swapped before the repair. The Hacker News called out that gap directly. Treat existing plugin content as a separate question from the current installer version.

Repair the machines, then repair the control

For Claude Code and Codex, install the repaired release or a later supported version, then prove that developers are running it. Remove or disable unneeded plugins before investigating the rest. Fewer extensions mean fewer repositories, update paths, and inherited permissions to account for.

GitHub Copilot and Gemini CLI require more caution because the 18 September reporting did not identify a Plugin4Shell patch for either product. For Copilot, avoid plugin sources on hosts that permit hash-shaped branch names, disable background updates where your deployment allows it, and consider disabling marketplace plugins until the vendor publishes a fix or a clear compensating control. Do not describe GitHub-hosted Copilot plugins as generally compromised; the reported main route depends on a name GitHub rejects.

Google announced in May 2026 that individual and free Gemini CLI service would transition to Antigravity CLI, with those Gemini CLI requests stopping on 18 June. Enterprise access was expected to continue during the transition (Google Developers Blog: Transitioning Gemini CLI to Antigravity CLI). Air’s September advice was to migrate because it expected no Plugin4Shell repair for Gemini CLI. If your company still permits Gemini CLI, locate every installation rather than assuming the product transition removed it.

Version work closes the known installer bug where a fixed release exists. It does not answer whether an older run placed unexpected code on disk. Compare installed plugin content with a clean, independently fetched copy of the approved commit. Fetch into a disposable environment that does not load the plugin, resolve the full object ID, and compare a deterministic archive or file manifest. Do not run an untrusted plugin to ask it whether it is trustworthy.

If content differs, resist the urge to explain the difference away as a build artifact until you have evidence. Package generation, line-ending conversion, and ignored files can create legitimate differences. A good comparison process defines which files should be deterministic and records any allowed transformation. Anything else remains unexplained.

Credential rotation should follow observed reach. If a changed plugin ran where a cloud token, package credential, SSH key, or signing secret was available, revoke and replace that authority. Check the services where it could have been used, not just the workstation where it lived. A token’s risk is measured by the systems that honor it.

Rebuild a machine when you cannot establish what executed or what persistence was possible. That decision should come from evidence and scope, not from the Plugin4Shell name alone. A mismatched working tree that never loaded may need a controlled removal and investigation. Unknown code that ran with broad developer authority calls for a more conservative recovery.

Then repair the process that let “requested pin” stand in for “observed code.” Your plugin catalog should retain both values. Record the approved repository and commit, then have the installer or a wrapper capture the resolved commit after checkout. A mismatch must fail closed before any plugin hook, setup command, or agent instruction can run.

The control belongs outside the plugin. A plugin must not be able to mark its own verification as passed, change the log that proves its identity, or widen the environment used for its inspection. The verifier needs less authority than the runtime and should produce a receipt somewhere the plugin cannot rewrite.

A practical plugin gate for coding agents

A useful gate begins before installation. Assign one owner for each approved plugin, record why the team needs it, and choose the repositories allowed to supply it. Approval should expire. A plugin that nobody has reviewed since its maintainer changed hands should not coast forever on an old decision.

The intake environment should be passive and disposable. It may fetch files and inspect Git objects, but it should not execute plugin hooks, open project secrets, or inherit a developer’s cloud session. Network access should be limited to the approved source and any separate reputation services the review process genuinely needs.

After the fetch, the gate needs a small set of facts. Store the repository URL after redirects, the expected full commit ID, the resolved local HEAD, the tree ID, a file manifest, the agent version, and the time of installation. Sign or ship that record to central storage. The exact database matters less than keeping the plugin from editing its own receipt.

Only then should static review and policy checks begin. Inspect executable files, installation hooks, tool declarations, Model Context Protocol endpoints, network destinations, and paths the plugin expects to read or write. A clean scanner result cannot prove intent, but it can catch obvious policy violations and produce material for human review.

Run the plugin under the same boundaries planned for daily use. Give it a test repository and fake credentials, not a production checkout and real tokens. Deny home-directory secrets by default. Restrict outbound network routes. Keep release signing and production deployment behind a separate human or service identity. A plugin compromise should encounter another boundary before it reaches the release line.

Updates need the same gate as first installation. A new version means new code, even when the marketplace calls it a routine refresh. Resolve and verify the new commit in the intake environment, compare permissions and destinations, then promote it. Automatic distribution can remain fast after verification; automatic trust should not.

The final test is deliberately simple: make the gate fail. Point a test catalog at the right repository with the wrong expected commit. Create a benign plugin update that changes an undeclared network destination. Attempt to load the plugin from an unapproved host. Confirm that each case stops before execution and leaves a useful receipt.

Teams often write a policy and never test the enforcement path. That creates a familiar paper shield. A quarterly failure exercise takes minutes and answers the question that matters: will the control stop the next bad package when nobody is watching the terminal?

The larger lesson is about verified state

Plugin4Shell did not defeat cryptography. The pinned commit still identified exact code. The installer failed to connect that identity to the files it had placed on disk.

That difference matters because engineering controls often stop one step too early. We record a desired state, issue a command, see a zero exit status, and call the result verified. The command may have succeeded at doing something adjacent to what we intended. Names may resolve differently, defaults may intervene, wrappers may select another binary, and local state may drift after the check.

Good automation asks for a state and then measures the state it got. A package install ends with a version and content check. A deployment ends with an observed image digest and a health probe. A permission change ends with an authorization test using the restricted identity. A coding-agent plugin install ends with a resolved commit, a content receipt, and a boundary test.

There is no need to turn every developer machine into a forensic lab. Start with the controls that buy the most certainty. Update the repaired agents. Inventory plugins and their hosts. Disable what has no owner. Compare existing content where the vulnerable path was plausible. Add the post-checkout identity comparison to your intake gate.

The marketplace pin remains useful. It gives reviewers a stable object to approve and operators a value to compare. Plugin4Shell showed that the pin cannot certify itself. The durable rule is plain: record what you intended to install, then independently prove what arrived before you let it run.

If you want practical security writing without a daily alarm bell, the newsletter is one email per month. The signup lives on this site.

Sources