Skip to content
T threat.cafe

· Nadia Ferreira-Strand (editorial persona) · Agent artifact supply chain · on threat.cafe

Pinned isn't verified: Plugin4Shell, trojanized hook updates and the update path nobody reviews

Review every plugin update, not just the install. In September 2026, Air Security showed that a pinned plugin commit could be swapped on update. An arXiv study saw malicious hook updates succeed in up to 92.5% of runs per tool, across 7 agent tools. Static scanners missed 47.5% of samples. Patch first. Then verify the resolved commit, lock auto-update in managed settings and gate each update with a diff review.

Outcome, time and access

The Claude Code runbook below makes every plugin, skill or hook update pass a check before it reaches a developer machine. Do a staging dry-run on one machine before the fleet.

Prerequisites:

  • Claude Code 2.1.179 or later, or Codex 0.146.0 or later. Air Security lists these as the Plugin4Shell fix versions.
  • Role-based access control (RBAC) rights to change managed settings. Server-managed settings need the claude.ai Owner role. MDM or managed-settings.json needs device admin rights.
  • git on the machine that mirrors your plugin repositories.
  • A named reviewer for plugin diffs.
  • A test machine that receives managed settings.

Stop if any one is missing.

Use this if / Do not use this if

Pick this runbook only if it fits your setup.

Use this if:

  • Developers install Claude Code plugins, skills or hooks from a marketplace.
  • You can push managed settings to their machines.
  • You mirror or vendor plugin repositories you depend on.

Do not use this if:

  • You need a Copilot vendor fix. Air Security reports none has shipped.
  • You run Gemini CLI. Google will not patch it; Air Security recommends Antigravity.
  • You need Codex, Copilot or Gemini CLI settings. We could not confirm their equivalents.

Why the update path needs a gate

Four September 2026 studies show one gap: teams review at install and trust every update after.

Plugin4Shell. Or Nevo, Dor Granat and Niv Hoffman of Air Security published on 17 September. Agents checked out a pinned commit without confirming the result. Git can resolve a name to a branch first, so the repository owner could swap the code. The fix compares HEAD to the full pinned SHA.

Trojanized hook updates. Pengxun Li, Litian Zhang, Jianwei Hou, Shujiang Wu, Song Li, Zifeng Kang and Xi Zhang posted arXiv 2609.03884 on 3 September. All 7 agent harnesses they tested fell, across 1,000 runs. Per-tool success reached 92.5%. Microsoft Defender caught none of the samples. Three static defences together missed 47.5%.

Claude mods. Yuval Fischer of Dash Security wrote on 25 September. Mods, a preview feature, load inside the Claude Code process, so endpoint tools see only Claude Code.

GitSpawn. Francisco Rosales of Manifold Security wrote on 1 September. He found 8 issues in 7 agents, where repository git config could run a program. Four were unpatched at publication.

Adversa's October roundup concludes that update review matters more than scanning.

Pre-flight checks (dry-run)

Run these read-only checks on the test machine before changing any setting.

  1. Run claude --version. Expect 2.1.179 or later. If lower, stop and update first.
  2. Run /status in a session. Expect Enterprise managed settings on the Setting sources line. If missing, fix delivery first.
  3. Save claude plugin list output as your baseline.
  4. Open ~/.claude/plugins/known_marketplaces.json. Note the autoUpdate value for each marketplace.
  5. Search shell profiles and dev containers for CLAUDE_CODE_ENABLE_FUNCTION_HOOKS, the flag that turns mods on. Search plugin caches and .claude/skills for hooks.json files with a modules key. Expect none. If found, hold that plugin for review.

Execute the runbook

Run these steps in order, verifying each one.

  1. Lock auto-update per marketplace. In managed settings, set "autoUpdate": false on each extraKnownMarketplaces entry. Verify: in /plugin, try the marketplace's auto-update toggle. Expect an error starting Auto-update for '<name>' is set by. If the toggle still works, the managed entry did not load. Go back to pre-flight check 2.
  1. Restrict sources. Set strictKnownMarketplaces to your approved sources, blockedMarketplaces for known-bad owners, and disableSideloadFlags to true. Verify: add an unlisted source with /plugin marketplace add. Expect is blocked by enterprise policy. If it succeeds, see failure mode 7.
  1. Restrict hooks. Set allowManagedHooksOnly to true. Set strictPluginOnlyCustomization to cover hooks at least. Verify: add a test hook in user settings. Expect it not to run. If it runs, check /status again.
  1. Block command sources. Set disableCommandPluginSources to true. Command-source plugins re-run their command every session, even with auto-update off. Verify: claude plugin list shows no command-source plugin loaded. If one loads, block it in managed enabledPlugins with false.
  1. Pin and verify. In your own marketplace, pin each entry with ref and sha. Pin archive sources with sha256. In your mirror, run this check after every fetch:
test "$(git -C path/to/plugin rev-parse HEAD)" = "<full-40-char-sha>" || echo "MISMATCH: stop"
git ls-remote --heads <plugin-repo-url> | grep -E 'refs/heads/[0-9a-f]{40}$' && echo "HASH-NAMED BRANCH: stop"
git ls-remote --symref <plugin-repo-url> HEAD

Verify: the first line prints nothing. The second prints nothing. The third shows the default branch you expect. If any check fails, do not publish the update.

  1. Gate each update. Run the threat.cafe Update Review Gate below on every new version. Then check the list on that exact version. Verify: the reviewer signs the gate. If "unknown", hold.
  1. Release on purpose. Change the entry's sha, then run claude plugin update <plugin>@<marketplace> on the test machine. Verify: claude plugin list shows the new version. Run /reload-plugins. If the version did not change, see failure mode 3.
threat.cafe Update Review Gate (run on every plugin, skill or hook update)
--------------------------------------------------------------------------
Artifact / source repo            : ________________________________
Old SHA or version -> new         : ______________ -> ______________
Resolved HEAD == full pinned SHA  : yes / no            (no = stop)
Hash-shaped branch / odd default  : none / found        (found = stop)
Diff read by a person             : yes / no            (no = stop)
New or changed hooks              : none / listed: _________________
New network, process or
credential access in diff         : none / listed: _________________
In-process modules (mods)         : none / listed: _________________
List check on THIS version        : known bad / no known bad / unknown
Decision                          : approve / hold / reject
Reviewer, date                    : ________________________________
Rollback SHA (last approved)      : ________________________________

Treat "unknown" as hold. Unknown isn't the same as safe.

Failure modes and fixes

Claude Code rollouts of these controls fail in nine common ways, each documented by Anthropic. Read them before you push to the fleet.

  1. You froze security fixes too. DISABLE_AUTOUPDATER in the managed env block stops plugin auto-update for every marketplace. It also stops Claude Code's own updates. The Plugin4Shell fix arrived that way. Fix: prefer per-marketplace "autoUpdate": false. If you must use the variable, run a scheduled manual update of Claude Code itself.
  1. Users can still turn auto-update on. A managed entry that leaves autoUpdate unset lets the user's toggle persist. Fix: set the field explicitly, true or false.
  1. An approved update does not arrive. Claude Code updates only when the computed version changes. A version in plugin.json wins. If the author keeps "version": "1.0.0", users stay on the cached copy. The update command prints is already at the latest version. Fix: bump version on release, or leave it out so the commit SHA is the version.
  1. Plugins vanish after the allowlist goes live. An allowlist without { "source": "skills-dir" } stops skills-directory plugins loading. No error shows. An empty allowlist, [], also blocks the official marketplace. A machine that saw that block will not re-register the official marketplace on its own. Fix: add the skills-dir entry if you need it. Register the official marketplace with an explicit extraKnownMarketplaces entry.
  1. Your MDM policy is ignored. By default only one managed source applies. Server-managed settings win if they deliver any key. Plugin keys in MDM or a file are then skipped. Fix: check /status. Set managedSourcesBehavior to "merge" if you need both.
  1. Hooks run the old version. After a mid-session update, hooks keep the previous version's path. Fix: run /reload-plugins. Restart for monitors.
  1. The allowlist rejects an approved source. Allowlist entries match exactly. A trailing slash, a .git suffix or ssh:// instead of https:// counts as a different source. Fix: use a hostPattern entry when a marketplace has more than one clone URL.
  1. User-enabled plugin hooks stop. allowManagedHooksOnly does not trust hooks from plugins users enable themselves. It also blocks headersHelper commands unless disableCommandPluginSources is set to false. Fix: enable approved plugins in managed enabledPlugins.
  1. Private marketplace updates fail quietly. Background checks do not prompt for git credentials. A helper that needs to prompt fails, and the old checkout stays. Fix: run gh auth login, then gh auth setup-git, on each machine.

Rollback

Plugin rollback in Claude Code takes one managed setting and one SHA change. Set your rollback trigger in advance, such as any unexpected hook, process or network call after an update.

  1. Block the plugin fleet-wide. Set it to false in managed enabledPlugins. Verify: claude plugin list shows it disabled. If not, check managed source precedence.
  2. Repoint your marketplace entry sha to the rollback SHA from the gate. Verify: claude plugin update and claude plugin list show the old version. If not, see failure mode 3.
  3. Re-enable the plugin. Run /reload-plugins. Verify against your pre-flight baseline.
  4. To remove a plugin for good, map its name to null in renames. Set forceRemoveDeletedPlugins to true. Verify: /plugin lists it under Flagged.
  5. To undo a policy change, remove the managed key and restart the session. Verify with /status.

Claude Code deletes an old plugin version directory 14 days after an update. Keep the approved SHA instead.

The steel-man: pinning is enough

The strongest counterargument says a pinned SHA makes updates immutable, so a gate adds only cost. Plugin4Shell shows otherwise. An unverified pin names the code you asked for, not what landed. Patched agents now check. Your mirrors may not.

Where this would be wrong

The case for this runbook assumes developers install third-party plugins. If you allow only plugins your own team writes, the gate is mostly code review you already do. If every agent tool you run verifies resolved commits and signs releases, step 5 adds less. If no updates pass through your marketplace, the gate does not fire. None of the sources report in-the-wild use of these attacks yet.

What to tell the people who sign off

Each group below gets one line, based on the sources above.

  • Finance: the cost is reviewer time per update.
  • Security lead: the blast radius of one bad update is every machine that auto-updates from that marketplace.
  • Legal and compliance: the signed gate is your evidence of review for each version.
  • Operations: rollback is one managed setting and one SHA change.

What we don't know yet

The open questions are about fix status. Copilot's status since Air Security's post is unknown. Claude Code mods may change before general release. GitSpawn patch status beyond Manifold's list is unknown. Pair this runbook with the cadence in "Approved last month, malicious today: a re-verification cadence".

Check each new version against the threat.cafe board before you approve it, not only the first install.

Volatility: VOLATILE (event-triggered review). Review when Microsoft ships a Copilot fix, Claude Code mods reach general release, or Claude Code changes its plugin update settings.

Sources

#plugins #hooks #plugin4shell #update-review #claude-code

All posts