Synced plugin update moves the plugin folder of an open session to .trash; its hooks then fail open
Claude Code version: 2.1.283, macOS. Plugin synced from claude.ai.
What happens
- Session A loads a synced plugin from
~/.claude/plugins/synced/<bucket>/<name>~g2. - Session B starts later. Its sync installs the new version into
<name>and moves<name>~g2to~/.claude/plugins/.trash. - In session A, every hook of that plugin now fails:
```
UserPromptSubmit hook error
Failed to run: Plugin directory does not exist: …/<name>~g2 (<name>@synced — run /plugin to reinstall)
PreToolUse
The same error follows for and PostToolUse`. Claude Code shows it and runs the tool anyway.
Why it matters
The plugin is a security plugin. Its UserPromptSubmit and PreToolUse hooks keep secrets out of the model, and its PostToolUse hook redacts tool output. After the move, tool output with a credential reached the model unredacted. The user did nothing, and saw only the hook error lines. The plugin cannot detect or prevent this, because none of its code runs.
Related
#46915 and #64718 describe the same cleanup race for marketplace plugins. The docs now say that a previous version stays 14 days "so a session that already loaded the old version keeps running" (plugins/loading#cleanup-of-previous-versions). Synced plugins do not seem to get this grace: the previous generation went to .trash while a session still used it.
Requests (either one closes the gap)
- Give synced plugins the same grace: keep the previous generation until no running session uses it, or for 14 days.
- Let a plugin declare that its hooks must not fail open (for example a manifest field). A hook that cannot start would then block
PreToolUseandUserPromptSubmit, and withhold thePostToolUseoutput.
3 Comments
This matches the marketplace cleanup race (#46915 / #64718), but for synced plugins the previous generation is getting hard-moved into
~/.claude/plugins/.trashwhile a live session still holds paths under…/synced/<bucket>/<name>~gN. Marketplace docs promise a 14-day keep-alive for the old version; synced does not appear to get that grace, so everyUserPromptSubmit/PreToolUse/PostToolUsehook for that plugin fails with “Plugin directory does not exist” and Claude Code continues the tool call anyway.That fail-open is the sharp edge for a security plugin: once the directory is gone, none of the plugin’s code runs, so redaction/secret gates cannot recover mid-session.
Mitigations that have worked in similar plugin-path races while waiting on a fix:
…/<name>~g2) out ofsynced/into a non-synced install location (local/marketplace path you control) and point hooks there — or end Session A before allowing Session B’s sync to run.~gNfolder from~/.claude/plugins/.trashback to its previous synced path while Session A is still open, then confirm the next hook invocation stops erroring. (A/pluginreinstall mid-session often binds the new generation only; it may not reattach the old session’s absolute paths.)Useful confirmations for maintainers if you still have the machine state:
~g2tree is still complete on disk (vs partially deleted),<name>generation reloads hooks without a full app restart,PreToolUse/UserPromptSubmitwhen the plugin dir is missing).Agree with the two asks in the OP: synced should get the same in-use/14-day grace as marketplace, and a manifest flag (or default for hooks that gate tool use) to fail closed when the plugin cannot start would close the credential-leak window you hit.
Follow-up with data from a second occurrence (Claude Code 2.1.284, macOS, org-synced plugin, 0.5.11 → 0.5.12).
/reload-pluginsdoes not recover the session. It keeps the old generation path./plugin→ Errors shows:On disk, the sync wrote the new version to
maisecretsand movedmaisecrets~g2to.trash/. The sync alternates betweenmaisecretsandmaisecrets~g2on each release. Three/reload-pluginsin a row gave the same error. Only a new process loads the plugin (claude --continue/claude --resume <id>). The hint "remove or rename the plugin on claude.ai" is misleading: the claude.ai entry is fine. #96997 looks like the same stale-generation lookup, at startup.Answers to the questions above:
~g2tree is complete (manifest,hooks/hooks.json, all scripts).plugin_folder_moved).Why every open session breaks: the sync runs at every Claude Code start. One new session swaps the folder under all sessions that are already open.
For comparison, a marketplace install is not affected. Same plugin from a local marketplace,
claude plugin update0.5.11 → 0.5.12 mid-session, then a second session started:cache/…/0.5.11stayed next to0.5.12, and the open session kept running its hooks. This matches the documented 14-day grace. Synced plugins need the same grace, or a stable path per generation that lives until the sessions that loaded it end.Same root cause on Linux (Claude Code 2.1.28x through 2.1.292), with a second symptom: besides hooks, the open session loses the plugin's skills and subagent types too.
Setup: a private plugin marketplace added on claude.ai (Customize → Plugins), synced to the CLI. The plugin (call it
my-plugin) carries skills and subagent types. Several long-running interactive sessions are open at once.Repro:
my-pluginloaded from~/.claude/plugins/synced/<marketplace>/<plugin>.my-plugin:some-skillanswersUnknown skill;my-plugin:some-agent) are no longer available, and the Agent tool can no longer start them.Frequency: seen repeatedly since early October 2026, across about 30 session transcripts. In five episodes, the plugin came back by itself at a later sync, 9 to 90 minutes after the drop, but not every time. A relaunch (
--resumeof the same session) always brings it back.Expected: keep the loaded copy until the session ends or relaunches (the 14-day grace that marketplace installs get, as above), or reload the new copy in place. Dropping it silently mid-session should not happen.
This adds to the two asks in the issue: whatever grace synced plugins get should cover skills and subagent types, not only hooks.