Synced plugin update moves the plugin folder of an open session to .trash; its hooks then fail open

Status Open
Reported on v2.1.283
Maintainer reply None cached
Activity 3 comments · opened Sep 28, 2026

Claude Code version: 2.1.283, macOS. Plugin synced from claude.ai.

What happens

  1. Session A loads a synced plugin from ~/.claude/plugins/synced/<bucket>/<name>~g2.
  2. Session B starts later. Its sync installs the new version into <name> and moves <name>~g2 to ~/.claude/plugins/.trash.
  3. 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)
`
The same error follows for
PreToolUse 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)

  1. Give synced plugins the same grace: keep the previous generation until no running session uses it, or for 14 days.
  2. Let a plugin declare that its hooks must not fail open (for example a manifest field). A hook that cannot start would then block PreToolUse and UserPromptSubmit, and withhold the PostToolUse output.

View original on GitHub ↗

3 Comments

stonianua · 16 days ago

This matches the marketplace cleanup race (#46915 / #64718), but for synced plugins the previous generation is getting hard-moved into ~/.claude/plugins/.trash while 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 every UserPromptSubmit / PreToolUse / PostToolUse hook 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:

  1. Stabilize the load path for long sessions. Before a second session can trigger a sync upgrade, copy the generation Session A actually loaded (…/<name>~g2) out of synced/ 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.
  2. If already trashed: restore that exact ~gN folder from ~/.claude/plugins/.trash back to its previous synced path while Session A is still open, then confirm the next hook invocation stops erroring. (A /plugin reinstall mid-session often binds the new generation only; it may not reattach the old session’s absolute paths.)
  3. Operational: treat “Synced plugin updated” + hook errors in an open session as a hard signal to stop tool use that depends on those hooks until the directory is restored or the session is restarted onto the new generation.

Useful confirmations for maintainers if you still have the machine state:

  • whether the trashed ~g2 tree is still complete on disk (vs partially deleted),
  • whether restarting Session A onto the new <name> generation reloads hooks without a full app restart,
  • and whether fail-open is intentional for all hook events or only some (security plugins effectively need fail-closed on PreToolUse / UserPromptSubmit when 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.

Sprinterli · 15 days ago

Follow-up with data from a second occurrence (Claude Code 2.1.284, macOS, org-synced plugin, 0.5.11 → 0.5.12).

/reload-plugins does not recover the session. It keeps the old generation path. /plugin → Errors shows:

synced[0] @ synced (user)
commands path not found: ~/.claude/plugins/synced/<org>_<user>/maisecrets~g2
This claude.ai copy did not load (see above) — remove or rename the plugin on claude.ai to clear it.

On disk, the sync wrote the new version to maisecrets and moved maisecrets~g2 to .trash/. The sync alternates between maisecrets and maisecrets~g2 on each release. Three /reload-plugins in 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:

  • The trashed ~g2 tree is complete (manifest, hooks/hooks.json, all scripts).
  • The session does not rebind to the new generation without a restart (see above).
  • Fail-open: when the directory is missing, no hook runs and the tool call runs. We measure this on every CI run (harness scenario 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 update 0.5.11 → 0.5.12 mid-session, then a second session started: cache/…/0.5.11 stayed next to 0.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.

howardpark · 6 days ago

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:

  1. Sessions are running with my-plugin loaded from ~/.claude/plugins/synced/<marketplace>/<plugin>.
  2. A new commit to the marketplace syncs (a new session starts, or a background sync round runs), and the synced folder is rewritten.
  3. In every session that was already running:
  • invoking a plugin skill such as my-plugin:some-skill answers Unknown skill;
  • the session is told its plugin subagent types (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 (--resume of 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.