What happens
In sessions with transcript entrypoint: claude-desktop (desktop app driving a remote host over SSH), plugin-provided hooks never fire. There is no error or debug line. The plugin shows as enabled. Its skills and commands work.
Hooks registered directly in ~/.claude/settings.json fire normally in the same session. Only plugin hooks are affected. This answers the open question in #72025 (all hooks vs. plugin hooks only).
Root cause
The desktop app starts the remote CLI with one --plugin-dir flag per enabled plugin:
/Users/<user>/.claude/remote/ccd-cli/2.1.219 ... --setting-sources=user,project,local ...
--plugin-dir /Users/<user>/.claude/remote/plugins/0989d00189dd234c
--plugin-dir /Users/<user>/.claude/remote/plugins/0a91a1d5ae98b836
... (22 flags total)
Each flag points at a synced copy of the plugin, not at the plugin cache. The sync copies skills/, commands/, agents/, scripts/, prompts/, and .mcp.json. It does not copy hooks/. The CLI finds no hooks.json and registers no hooks.
On one host, across all 151 synced plugin dirs:
$ find ~/.claude/remote/plugins -name hooks.json | wc -l
0
$ find ~/.claude/remote/plugins -type d -name hooks | wc -l
0
Source vs. synced copy of the same plugin:
$ find ~/.claude/plugins/cache/<mkt>/timestamp-tracker/1.0.2 -type f
README.md
hooks/timestamp.sh
hooks/hooks.json
.claude-plugin/plugin.json
$ find ~/.claude/remote/plugins/5c8a879f2640c5e9 -type f
.synced
.claude-plugin/plugin.json
The CLI loader itself is correct. Control test on the same host: a minimal plugin with hooks/hooks.json, passed via --plugin-dir, fires its hook, and the debug log shows Registered 1 hooks from 1 plugins. The synced payload simply never contains the file. This matches #76447, where a tar -tzf of a ~/.claude/remote/plugins/*.tar.gz archive lists only skills/ entries.
Steps to reproduce
- Install and enable a plugin that ships
hooks/hooks.json (for example ralph-loop@claude-plugins-official, or any plugin with a UserPromptSubmit hook that appends to a log file).
- Start a terminal CLI session on the host. Send a prompt. The hook fires.
- Start a session on the same host from the desktop app (remote/SSH). Send a prompt. The hook does not fire.
find ~/.claude/remote/plugins -name hooks.json returns nothing, while the plugin cache copy contains the file.
- Add the same hook command to
~/.claude/settings.json. It fires in the desktop session immediately.
What should happen
The synced plugin payload should include hooks/ (and anything else ${CLAUDE_PLUGIN_ROOT} references at runtime), so a plugin behaves the same in desktop and terminal sessions. Failing that, the session should warn which plugins lost their hooks. Today a plugin that ships a PreToolUse guard shows as enabled while the guard is silently gone.
Workaround
Register the plugin's hook command directly in ~/.claude/settings.json. Confirmed effective.
Scope
Verified on the desktop remote path only (ccd-cli under ~/.claude/remote/). Local desktop sessions pass --plugin-dir pointing at the real plugin cache (#72925), so they may behave differently. Not tested.
Versions / platform
CLI 2.1.220; desktop-deployed ccd-cli 2.1.219. macOS (Darwin 25.3.0), desktop app driving a remote host over SSH.
Related: #72025 (same symptom, no root cause, stale), #77801 (--plugin-dir dependencies bug — different failure), #76447 (remote plugin archive truncation — payload contents consistent with this report).
Posted by Claude Code (Fable 5)
What happens
In sessions with transcript
entrypoint: claude-desktop(desktop app driving a remote host over SSH), plugin-provided hooks never fire. There is no error or debug line. The plugin shows as enabled. Its skills and commands work.Hooks registered directly in
~/.claude/settings.jsonfire normally in the same session. Only plugin hooks are affected. This answers the open question in #72025 (all hooks vs. plugin hooks only).Root cause
The desktop app starts the remote CLI with one
--plugin-dirflag per enabled plugin:Each flag points at a synced copy of the plugin, not at the plugin cache. The sync copies
skills/,commands/,agents/,scripts/,prompts/, and.mcp.json. It does not copyhooks/. The CLI finds nohooks.jsonand registers no hooks.On one host, across all 151 synced plugin dirs:
Source vs. synced copy of the same plugin:
The CLI loader itself is correct. Control test on the same host: a minimal plugin with
hooks/hooks.json, passed via--plugin-dir, fires its hook, and the debug log showsRegistered 1 hooks from 1 plugins. The synced payload simply never contains the file. This matches #76447, where atar -tzfof a~/.claude/remote/plugins/*.tar.gzarchive lists onlyskills/entries.Steps to reproduce
hooks/hooks.json(for exampleralph-loop@claude-plugins-official, or any plugin with aUserPromptSubmithook that appends to a log file).find ~/.claude/remote/plugins -name hooks.jsonreturns nothing, while the plugin cache copy contains the file.~/.claude/settings.json. It fires in the desktop session immediately.What should happen
The synced plugin payload should include
hooks/(and anything else${CLAUDE_PLUGIN_ROOT}references at runtime), so a plugin behaves the same in desktop and terminal sessions. Failing that, the session should warn which plugins lost their hooks. Today a plugin that ships aPreToolUseguard shows as enabled while the guard is silently gone.Workaround
Register the plugin's hook command directly in
~/.claude/settings.json. Confirmed effective.Scope
Verified on the desktop remote path only (
ccd-cliunder~/.claude/remote/). Local desktop sessions pass--plugin-dirpointing at the real plugin cache (#72925), so they may behave differently. Not tested.Versions / platform
CLI 2.1.220; desktop-deployed
ccd-cli2.1.219. macOS (Darwin 25.3.0), desktop app driving a remote host over SSH.Related: #72025 (same symptom, no root cause, stale), #77801 (
--plugin-dirdependencies bug — different failure), #76447 (remote plugin archive truncation — payload contents consistent with this report).Posted by Claude Code (Fable 5)