top of page

Plugin4Shell: when the pin lies about the plugin

Writer: Inception Security
Inception Security
11 minutes ago
9 min read
Plugin4Shell hunt comic

SHA pinning should freeze a reviewed plugin at one commit. You pin the hash, you review that snapshot, and what runs on the machine is what you read. That assumption is wrong in four major AI coding agents. They check out the pinned SHA, but none of them confirm that the tree they got back matches it, so a repository owner can point that hash at different code while the pin still looks honored.


Air Security disclosed the class on September 17, 2026. There is no CVE, and nobody has reported it being used in anger, so this is not a fire drill. What makes it worth an afternoon is that the fix is uneven across the four agents, and the assumption it breaks is one most code reviews lean on without noticing.


If your developers run coding agents, four questions decide how much of this matters to you:


  • Which agents are installed, and at which versions?

  • Which plugins are pinned, and to which remotes?

  • Do sensors show agent runtimes spawning git or shells around plugin updates?

  • After a suspected swap, which credentials and internal systems could that developer reach?


How the attack works


Trust the marketplace pin


An agent installs a marketplace plugin locked to a commit hash, and everyone downstream treats that hash as the reviewed snapshot. The marketplace trusts it, the developer trusts it, and any security review that stops at the pin inherits the same assumption without ever testing it.


Checkout without tree verification


The agents fetch or check out the pinned ref, but none of them confirm that the files now on disk match that commit. Air Security frames it as one missing step after checkout: resolve what actually landed, compare it to the pin, and refuse to run if the two disagree.


That missing step matters because of how git resolves names. On hosts that allow a branch or tag named like a commit hash, the repository owner can make checkout land on something other than the commit you pinned. GitHub rejects hash-shaped names outright, which closes the path there. Bitbucket and most self-hosted git servers do not, and those are still supported marketplace backends, which is where the real exposure sits for Claude Code, Codex and Copilot.


Gemini CLI gets there by a different route. Air reports a variant that abuses a default branch named after an internal fetch marker, which means GitHub's hash-name rule is not automatic clearance if you run Gemini CLI. You do not need to reconstruct either recipe to hunt this. The spine is the same in both: a pin was recorded, a checkout ran, and nothing ever verified the tree.


Zero-click swap via auto-update


Auto-update is what turns this from a bad install into a zero-click problem. Claude Code and Codex update built-in marketplace plugins by default, so a plugin the developer vetted months ago can be swapped in the background without anyone seeing a prompt.


It is worth scoping that honestly rather than overselling it. Default catalogs are mostly GitHub-hosted and point at GitHub remotes, where the naming rule already blocks the branch-name path, and auto-update is on for built-in marketplaces but optional or off for outside ones. The sharper exposure is anything on a non-GitHub remote, plus Copilot's support for hosts beyond GitHub. GitHub told The Register it blocks hash-shaped names, and Air's answer is that the agents still support Bitbucket and similar servers, so a GitHub-only reading does not close the class.


Code runs as the developer


Once the swap lands there is no second stage to wait for. The plugin inherits whatever the agent can already reach: local files, credentials, cloud CLIs and anything the laptop can talk to. There is no dropper to catch, because the plugin is the execution.


Risk: agent privileges on developer endpoints


Treat a developer laptop running a coding agent as a high-privilege endpoint, because that is what it is. One swapped plugin opens a path to source theft, CI token theft, cloud console access and lateral movement out of the workstation. None of that is evidence any of it happened to you. It is the list of places to look if you find that it did.


Patching is uneven. Per Air Security:


  • Claude Code 2.1.179+ is fixed.

  • Codex 0.146.0+ is fixed.

  • GitHub Copilot had no fix as of September 18 coverage.

  • Gemini CLI will not be patched (deprecated). Migrate to Antigravity per Air and Google guidance.


There were no vendor security advisories and no CVE as of September 18. The urgency comes from the assumption being shared across the whole toolchain and the remediation being half finished, not from anyone confirming an attack.


Mitigate and reduce risk


Start with the two agents that have a fix. Claude Code goes to 2.1.179 or later, Codex to 0.146.0 or later. For Copilot and Gemini CLI there is nothing to install, so the work is inventory instead: find out who has them, which plugins are installed, and turn auto-update off wherever the product lets you. Gemini CLI users need a migration to Antigravity rather than a patch, because no fix is coming.


After that, look at where your plugins come from. Remotes on hosts that reject hash-shaped branch and tag names are the safer bet, so Bitbucket and self-hosted marketplace remotes deserve to be treated as higher risk until the agents start verifying checkout trees themselves.


Two things are worth changing while you are in there. Alert on unexpected shells spawning from agent runtimes after a plugin update, and get long-lived cloud keys out of env files the agent can read. Short-lived tokens on developer hosts limit what a swapped plugin is worth.


Before you start patching, preserve the evidence: agent update logs, pin records, plugin directory history, process ancestry and egress around the update windows. Updating only stops the next swap. It tells you nothing about what a plugin already sitting on the machine could have done, and patching over it makes that history harder to reconstruct.


How to hunt the activity


The chain worth reconstructing runs in one direction. A pin gets recorded, a checkout runs without verifying the tree, the plugin inherits the agent's privileges, and secrets and egress follow from there. Each step leaves something different behind, which is why no single query answers this.


Start from September 17, 2026, the disclosure date, and only reach back further if you can say why. Keep timestamps, host, parent and child process, command line, plugin path, remote host and version telemetry, because you will want all of it if something looks wrong.


Hunt in KQL


This one looks for an agent runtime spawning git or a shell. Tune the process names to whatever your developers actually run.


let StartTime = datetime(2026-09-17);
DeviceProcessEvents
| where Timestamp >= StartTime
| where InitiatingProcessFileName has_any (
    "claude", "codex", "node", "copilot", "gemini", "Code.exe", "Cursor.exe"
  )
| where FileName has_any ("git", "git.exe", "bash", "zsh", "sh", "powershell.exe", "cmd.exe", "pwsh.exe")
    or ProcessCommandLine has_any ("checkout", "fetch", "plugin", "skill", "marketplace")
| project Timestamp, DeviceName, InitiatingProcessFileName, InitiatingProcessCommandLine,
    FileName, ProcessCommandLine, FolderPath, AccountName
| order by Timestamp asc

This one is the inventory question. If you have soft or custom inventory to join against, flag anything running Claude Code below 2.1.179 or Codex below 0.146.0, plus every remaining Gemini CLI install and any unpatched Copilot plugin stack.


Hunt in Falcon CQL


Point this at the endpoint view covering developer workstations. Endpoint telemetry is where this shows up, because nothing about the swap is visible from the network alone.


(#event_simpleName=ProcessRollup2)
| (ParentBaseFileName=/claude|codex|node|copilot|gemini|Code|Cursor/i
   OR CommandLine=/plugin|skill|marketplace|claude|codex|copilot|gemini/i)
| (FileName=/git|bash|zsh|sh|powershell|cmd|pwsh/i
   OR CommandLine=/checkout|fetch|clone|plugin|skill/i)
| table([@timestamp, ComputerName, UserName, ParentBaseFileName, FileName, CommandLine], limit=1000)

(#event_simpleName=NetworkConnectIP4) OR (#event_simpleName=DnsRequest)
| (CommandLine=/plugin|skill|marketplace|bitbucket|git/i
   OR ParentBaseFileName=/claude|codex|node|copilot|gemini/i)
| table([@timestamp, ComputerName, DomainName, RemoteAddressIP4, CommandLine], limit=1000)

Then pivot on Bitbucket or self-hosted git traffic around plugin update time. A GitHub-only catalog already has the branch-name path closed, so anything pointing somewhere else is where the attention belongs.


Hunt in Splunk


Swap in your own indexes and set the time picker to September 17, 2026, again reaching back further only if you have a reason.


index=YOUR_EDR_OR_ENDPOINT_INDEX earliest=09/17/2026:00:00:00
| eval parent=coalesce(parent_process_name, ParentImage, parent_process)
| eval proc=coalesce(process_name, Image, process)
| eval cmdline=coalesce(process_command_line, CommandLine, cmdline)
| where match(parent, "(?i)(claude|codex|node|copilot|gemini|Code|Cursor)")
  AND (match(proc, "(?i)(git|bash|zsh|sh|powershell|cmd|pwsh)")
       OR match(cmdline, "(?i)(checkout|fetch|clone|plugin|skill|marketplace)"))
| table _time host user parent proc cmdline
| sort 0 _time

index=YOUR_PROXY_OR_DNS_INDEX earliest=09/17/2026:00:00:00
| eval dest=coalesce(url, dest, dest_host, query, domain)
| where match(dest, "(?i)(bitbucket\.org|bitbucket|git\.)")
  AND NOT match(dest, "(?i)(github\.com|githubusercontent)")
| table _time host user dest status _raw
| sort 0 _time

The pairing that matters is egress to a non-GitHub remote alongside agent process ancestry, on the same host, inside the same window. Either one on its own is noise.


Hunt in Kibana


Set the Discover time range to September 17, 2026 and work forward from there:


(process.parent.name: (*claude* OR *codex* OR *node* OR *copilot* OR *gemini* OR *Code* OR *Cursor*)
 OR process.parent.executable: (*claude* OR *codex* OR *copilot* OR *gemini*))
AND (process.name: (git OR bash OR zsh OR sh OR powershell OR cmd OR pwsh)
 OR process.command_line: (*checkout* OR *fetch* OR *plugin* OR *skill* OR *marketplace*))

(destination.domain: (*bitbucket* OR *git.*) OR dns.question.name: (*bitbucket* OR *git.*))
AND NOT destination.domain: (*github.com* OR *githubusercontent*)
AND (process.parent.name: (*claude* OR *codex* OR *node* OR *copilot* OR *gemini*)
 OR process.name: (git OR node))

Columns: @timestamp, host.name, user.name, process.parent.name, process.name, process.command_line, destination.domain, file.path.


Integrity mismatch teaching (all platforms)


The cleanest signal across every platform is a lock file that still shows the expected SHA while the plugin directory changed underneath it after an auto-update. Compare your pin records against tree change times and look for the gap. You do not need to work out how an attacker would force that mismatch in order to spot one. The file artifact is sudden writes under plugin or skill install paths after a background update, and it only means something once you correlate it with process ancestry and egress.


Field mapping, false positives, and empty results


Field mapping: validate your parent process and command-line fields against a known agent install before you trust a negative result, because plugin paths differ by product and by operating system. Version telemetry often lives in soft inventory or the CMDB rather than in EDR alone.


False positives: legitimate plugin updates, IDE git activity and approved marketplace syncs all produce the same agent-to-git ancestry you are hunting for. Before escalating, require something extra: an unexpected remote, a pin that disagrees with the tree, a shell after the update, or a change to the credential helper.


Empty results: the agents may simply not be deployed, or your catalogs may all be GitHub-hosted, in which case the branch-name path was closed before you started. But missing parent command lines, seven-day retention and absent pin telemetry produce exactly the same empty result while the risk is still there. Endpoint sensors alone do not give you visibility into marketplace locks, so an empty query is not an answer.


Interpret the results before escalating


Finding

Next investigative step

Agent runtime spawns git or shell around plugin update

Identify plugin path, remote, and whether auto-update was expected.

Plugin directory change while lock still shows old SHA

Treat as integrity mismatch; preserve tree and pin records; review adjacent process and egress.

Egress to Bitbucket or self-hosted git from agent context

Confirm whether that remote is an approved marketplace; expand hunt on that host.

Claude Code below 2.1.179, Codex below 0.146.0, Gemini CLI present, or Copilot plugin stack unpatched

Patch or migrate; inventory installed plugins; review secrets reachable by that user.

Empty SIEM searches

Validate agent deployment, field mapping, retention, and whether pins and remotes are logged at all.


Do not escalate because a developer ran git or updated a plugin. That is most of what a developer does. What turns it into a finding is the pin, the remote, and what the agent could reach.


Sources



What This Means for Your Team


The durable lesson here is not about four specific agents. It is that the developer endpoint has quietly become a supply chain, and the evidence it leaves behind is a pin that looks honored, a checkout nobody verified, and then process, file and egress traces showing that a trusted plugin changed under the user.


That reasoning outlives this disclosure. Work out which agents and which remotes your developers already trust, what those agents can reach from the laptop, and whether the pins you have on record match what actually landed on disk. Where they do not, follow it into credentials, cloud CLIs and the systems that accept that developer's identity.


Most teams cannot answer that last question today, and it is rarely because nobody thought of it. The telemetry is not forwarded, retention is too short, or nobody has ever had a reason to look at a developer laptop that closely. If you run Microsoft Sentinel and Defender, Inception Security can connect those questions to telemetry you are already paying for, and an Inception Foresight M365 Assessment will show you which of those gaps you actually have.


Inception Protection


Inception Protection is Inception Security's MDR on the Defender and Sentinel stack you already pay for. For Plugin4Shell-class cases, we help connect agent process ancestry, plugin path integrity, and non-GitHub remote egress to credential and identity review on developer endpoints.


Inception Foresight


Want a clearer view of visibility and detection gaps on the Microsoft stack that affect investigations like this? Grab our free Inception Foresight M365 Assessment. No strings. Follow Inception Security on LinkedIn and @inceptionsec on X.

bg-map-white.png

INCEPTION SECURITY™

A cybersecurity company with in depth knowledge of the threat landscape and security controls.

NAVIGATION

GET IN TOUCH

© 2025 All Rights Reserved by INCEPTION SECURITY™ .

bottom of page