Claude Code Plugin Security After Plugin4Shell
How the Plugin4Shell SHA-pinning bypass hit Claude Code, Codex, Copilot, and Gemini CLI, and how to check your Claude Code plugins are safe: confirm version 2.1.179 or later, audit installed plugins, run claude plugin validate, and lock down auto-update and marketplaces.
Quer o framework por trás destes projetos?
Obtenha o sistema Claude Code que usamos para planejar, construir, testar e lançar software em produção.
Plugin4Shell broke the one promise a plugin marketplace makes: that the code you reviewed is the code that runs. Claude Code, Codex, Copilot, and Gemini CLI all asked Git for a pinned commit and never checked what they got back. Claude Code has been fixed since version 2.1.179. If you are on that version or later, the specific bug is closed, but the lesson about what plugins can do on your machine is not.
This post covers how the bypass actually worked, how to confirm you are patched, and a practical audit for the plugins you already have installed.
The short version
A plugin in Claude Code is code that runs with your user privileges. Hooks fire on session start and around tool calls, MCP servers launch as local processes, and anything in a plugin's bin/ folder lands on the Bash tool's PATH. The marketplace pin (a 40-character commit SHA) is the mechanism that is supposed to freeze that code at a version someone reviewed.
Plugin4Shell, disclosed publicly on September 17, 2026 by researchers at Air Security, showed that the pin was requested but never verified. An attacker who controlled the plugin's repository could make the checkout resolve to different code while every UI still reported the reviewed SHA. Combined with background auto-update, that meant code execution with no prompt and no click.
Patch status at disclosure:
| Agent | Status |
|---|---|
| Claude Code | Fixed in 2.1.179 (confirmed June 17, 2026) |
| OpenAI Codex | Fixed in 0.146.0 (verified August 12, 2026) |
| GitHub Copilot | No client fix shipped at disclosure |
| Google Gemini CLI | Deprecated, Google said it will not be patched |
How SHA pinning is supposed to work
A marketplace is a marketplace.json file listing plugins and where to fetch them. For git-based sources, an entry can carry both a ref (branch or tag) and a sha (a full commit hash). The Claude Code docs are explicit that sha takes precedence when both are set:
{
"name": "git-plugin",
"source": {
"source": "url",
"url": "https://gitlab.com/team/plugin.git",
"ref": "main",
"sha": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"
}
}The trust model is simple. A curator reviews the plugin at one commit, pins that commit, and bumps the pin only after reviewing the next version. The plugin author can push whatever they want to main. Users only ever get what the curator approved. Anthropic's community marketplace works exactly this way: every plugin in the catalog is pinned to a specific commit SHA after automated validation and safety screening.
That model only holds if the client enforces the pin. That is where it broke.
The real mechanism: Git prefers a ref over a commit
The vulnerable install path in Claude Code, Codex, and Copilot boiled down to two commands:
git clone <plugin repo> ./
git checkout a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0git checkout accepts either a branch name or a commit. When the argument is a 40-character hex string, you would expect Git to treat it as a commit. It does not have to. If a branch with that exact name exists, Git resolves the ambiguous name to the branch and prints only a warning that the refname is ambiguous. Nothing fails. The exit code is clean.
So the attacker creates a branch literally named a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0, points it at malicious code, and makes it the repository's default branch. The client asks for the pinned commit, Git hands back the branch, and the client records a successful install at the expected SHA. At no point did anything compare the commit actually sitting in the working tree against the pin.
Gemini CLI used a different fetch sequence and was hit by a variant of the same idea:
git clone --depth 1 <plugin repo> ./
git fetch origin <pinned sha>
git checkout FETCH_HEADIf the repository's default branch is itself named FETCH_HEAD, the checkout resolves to that branch and the correctly fetched commit is silently discarded.
The fix Air Security described is one assertion: after checkout, resolve the commit that is actually in the working tree (the resolved HEAD, not the ref that was requested) and abort unless it equals the pinned SHA. That check has to live in the agent. The marketplace cannot enforce it because the pin is resolved on your machine.
Why it was zero-click
The branch trick alone would still need you to install something. Auto-update removed that step. The attack chain the researchers laid out:
- Plant. The attacker publishes a genuinely benign plugin, pinned at commit A.
- Adoption. People install it. Every install matches the reviewed code.
- Version bump. The marketplace curator reviews a new benign commit B and re-pins to it.
- Rug pull. The attacker creates a branch named after commit B, points it at malicious code, and sets it as the default branch.
- Auto-update. Background plugin updates fetch the new pin, the checkout resolves to the branch, and the malicious code is installed with no prompt.
Note step 3. The curator did their job correctly. The review was of real, benign code. The attack targets the gap between what was reviewed and what the client fetched, which is exactly the gap pinning exists to close.
Once installed, a plugin does not need you to invoke anything. A SessionStart hook runs the next time you open a session. From there it has what you have: the source tree, cloud credentials in your environment, SSH keys, tokens in dotfiles.
Who was actually exposed
Two conditions narrowed the real-world blast radius.
The Git host had to allow SHA-shaped branch names. GitHub rejects branch names that look like full commit SHAs, so a plugin hosted on GitHub could not be used for the branch variant. Air Security reported that Bitbucket and self-hosted Git servers do allow them. In Claude Code terms, that means plugin entries using the url or git-subdir source pointing at non-GitHub hosts were the realistic path.
The client had to be unpatched. Anthropic shipped the fix in 2.1.179 in June, three months before the public write-up. If your install auto-updates Claude Code itself, you were almost certainly patched long before you heard the name. No reporting at disclosure described exploitation in the wild.
That is the good news. The uncomfortable part is that the same trust assumption was shared by all four major agents, which tells you how young this supply chain still is.
Step 1: confirm you are on a fixed version
claude --versionAnything at 2.1.179 or later has the Plugin4Shell fix. If you are below it, update now:
# npm install
npm install -g @anthropic-ai/claude-code@latest
# Homebrew
brew upgrade claude-codeIf you use the native installer, re-run the install command from the setup docs. Check this on every machine that runs Claude Code, including CI runners and dev containers that may be pinned to an old image. A container image built in May is still vulnerable no matter what your laptop runs.
If your team also uses Codex, the fixed version is 0.146.0. If anyone uses Copilot's agent plugins or the deprecated Gemini CLI, treat those plugin systems as unpatched and restrict them to sources you control.
Step 2: inventory what is installed
You cannot audit what you have not listed. Start with the plugins:
claude plugin list
claude plugin list --jsonInside a session, /plugin opens the manager and the Installed tab groups plugins by scope. Plugins with load errors float to the top, and marketplace plugins you have not used in a while appear under a Not used recently header. Unused plugins are the easiest risk to remove: they still load hooks and MCP servers every session and give you nothing back.
Then list the marketplaces, because a marketplace is what decides which code you get on update:
claude plugin marketplace listFor each marketplace, ask three questions. Who controls this catalog? Does it pin SHAs, or does it track a branch? Where are its plugins hosted? A marketplace that points at main of a third-party repo with no sha gives you no pinning at all, patched client or not.
The underlying state lives in ~/.claude/plugins/. installed_plugins.json records what is installed and at which version, known_marketplaces.json records your catalogs, and the fetched code sits under ~/.claude/plugins/cache/. Reading those files directly is useful when you want to diff state across machines.
Step 3: read what each plugin will actually run
For every plugin that stays, look at what it contributes:
claude plugin details <plugin-name>@<marketplace-name>The detail view lists commands, skills, agents, hooks, MCP servers, and LSP servers. Skills and agents are mostly Markdown and prompt text. The parts that execute code are the ones worth reading line by line:
- Hooks in
hooks/hooks.json. Look forcommandhooks, especially onSessionStart,UserPromptSubmit, andPreToolUse. A hook that curls a remote script and pipes it to a shell is a red flag regardless of intent. - MCP servers in
.mcp.json. Eachcommandis a process launched with your privileges. Check what it runs and whetherenvpasses it secrets. - Executables in
bin/. These are added to the Bash tool'sPATHwhile the plugin is enabled, which means a plugin can shadow a common command name. - Dependency installs. Plugins can install Node.js packages. Claude Code runs these with
--ignore-scripts, so lifecycle scripts do not execute during install, but aSessionStarthook can still run whatever it wants afterwards.
Look in the cache directory for the plugin and read the files there, not the repo's current main. The cache is what actually runs.
Step 4: run claude plugin validate, and know its limits
claude plugin validate ./path-to-plugin
claude plugin validate ./path-to-plugin --strictInside a session, /plugin validate . does the same. It checks JSON syntax and schema, plugin.json and marketplace.json structure, component paths, the renames map, reserved marketplace names, and rejects ../ paths that escape the marketplace root. --strict turns warnings into errors, which is the right mode for CI on a marketplace repo.
Be clear about what that is. Validation proves a plugin is well formed. It does not scan for malicious behavior, and it would not have caught Plugin4Shell, since the bug was in the client's checkout, not in any manifest. Use it as a gate on your own marketplace and plugins, and do the reading from Step 3 for anything third party.
Step 5: make auto-update a deliberate choice
Auto-update is what turned a pin bypass into a zero-click attack. In current Claude Code the defaults are already sensible: claude-plugins-official, most official Anthropic marketplaces, and marketplaces added from claude.ai auto-update by default, while other third-party marketplaces have auto-update disabled by default.
Check what you actually have. In /plugin, open Marketplaces, select each one, and choose Enable auto-update or Disable auto-update. For a third-party catalog you have not vetted, off is the right answer. Update it on your schedule instead:
claude plugin marketplace update <marketplace-name>
claude plugin update <plugin-name>@<marketplace-name>Then run claude plugin details again and compare before reloading. Updates that land in the background also do not take effect in the running session: Claude Code checks after startup and asks you to run /reload-plugins, which is a natural checkpoint to look at what changed.
To stop automatic updates for Claude Code and marketplace plugins, set DISABLE_AUTOUPDATER=1. Plugins with a command source are the exception: Claude Code re-runs their command once per session regardless of that setting. If you want Claude Code frozen but plugins still updating, add FORCE_AUTOUPDATE_PLUGINS=1 alongside it. Be careful with the first option on a team: freezing Claude Code is also how machines end up stuck below a security fix like 2.1.179.
Step 6: lock down marketplaces for a team
For organizations, the strongest control is strictKnownMarketplaces in managed settings. It restricts which marketplaces anyone can add. An empty array blocks everything, including the official marketplace. A list is an allowlist:
{
"strictKnownMarketplaces": [
{ "source": "github", "repo": "anthropics/claude-plugins-official" },
{ "source": "github", "repo": "acme-corp/*" }
]
}Pair it with extraKnownMarketplaces to register your internal catalog automatically, and host that catalog's plugins somewhere you control. A few related managed settings are worth knowing: disableCommandPluginSources blocks plugins whose source is an arbitrary shell command, and disableSideloadFlags blocks CLI flags that load plugins or MCP servers for a single run.
If you run an internal marketplace on Bitbucket or a self-hosted Git server, you were in the exposed category for unpatched clients. Check that no branch or tag in your plugin repos is named like a 40-character hex string or FETCH_HEAD, and restrict who can change the default branch.
The checklist
claude --versionshows 2.1.179 or later on every machine, container, and CI runner.claude plugin listreviewed. Unused plugins uninstalled.claude plugin marketplace listreviewed. Every third-party catalog is one you trust, and it pins bysha.- Hooks, MCP server commands, and
bin/executables read for each third-party plugin, from the cache. claude plugin validate --strictrunning in CI for any marketplace or plugin you publish.- Auto-update off for third-party marketplaces unless you trust the curator and the host.
- Teams:
strictKnownMarketplacesset in managed settings, internal plugin repos checked for SHA-shaped branch names. - If a plugin from an untrusted host ran on an unpatched client, rotate the credentials it could reach.
Failure modes worth remembering
Trusting the pin without trusting the client. A pin is a request, not a guarantee. Plugin4Shell existed because four clients treated a successful checkout as proof of content. Only a patched client closes that gap.
Freezing versions to feel safe. Disabling updates stops surprise plugin changes, but it also stops Claude Code security fixes. Freeze plugins per marketplace, not the whole tool.
Treating validation as a security scan. claude plugin validate is a linter for structure. A perfectly valid plugin can still ship a hostile hook.
Forgetting non-interactive environments. CI runners and containers install plugins from project settings and often run older images. They also tend to hold the most valuable credentials.
Reviewing the repo instead of the cache. What runs is in ~/.claude/plugins/cache/, at the version recorded in installed_plugins.json. The repo's default branch can say anything.
Frequently asked questions
What is Plugin4Shell?
Plugin4Shell is a zero-click remote code execution flaw in the plugin systems of Claude Code, OpenAI Codex, GitHub Copilot, and Google Gemini CLI, publicly disclosed by Air Security on September 17, 2026. The agents checked out a plugin's pinned commit SHA but never verified that the working tree actually landed on that commit, so an attacker controlling the plugin repo could serve different code while the pin still looked honored.
Is Claude Code patched against Plugin4Shell?
Yes. Anthropic fixed it in Claude Code 2.1.179, confirmed on June 17, 2026, months before public disclosure. Run claude --version and make sure you are on 2.1.179 or later. OpenAI patched Codex in 0.146.0. Microsoft had not shipped a Copilot fix at disclosure, and Google deprecated Gemini CLI without fixing it.
Does claude plugin validate detect malicious plugins?
No. claude plugin validate checks structure: JSON syntax, manifest and marketplace schema, component paths, and path escapes outside the marketplace. It tells you a plugin is well formed, not that it is safe. Security review means reading the hooks, MCP server commands, and bin/ executables yourself.
Were plugins hosted on GitHub vulnerable?
Much less so. The attack needs a branch named after a full 40-character commit SHA, and GitHub rejects 40-character hex branch names. Bitbucket and self-hosted Git servers were reported to allow them, so plugins sourced from those hosts were the realistic exposure on unpatched clients.
Should I turn off plugin auto-update?
For third-party marketplaces, keeping auto-update off and updating deliberately is the safer default, and it is already the default for third-party marketplaces in current Claude Code. Official Anthropic marketplaces auto-update by default. Auto-update is what turned Plugin4Shell into a zero-click attack, so treat it as a trust decision per marketplace.
Posted by @speedy_devv
Quer o framework por trás destes projetos?
Obtenha o sistema Claude Code que usamos para planejar, construir, testar e lançar software em produção.
Best Plugins (2026)
A ranked shortlist of the best Claude Code plugins in 2026, pulled from the 276 entries in the official marketplace, with real install counts and a list of what to uninstall.
Best Claude Code Skills
The best Claude Code skills in 2026, measured: Superpowers, Anthropic's official skills (skill-creator, frontend-design, webapp-testing, mcp-builder) and gstack, with install commands, what each replaces, its token cost, and which to skip.

