[BUG] Desktop remote sessions: plugin sync to ~/.claude/remote/plugins/ omits hooks/, so plugin hooks never fire

Status Open
Reported on v2.1.219
Maintainer reply ✓ Yes — amorriscode
Activity 4 comments · opened Aug 3, 2026
💡 Likely answer: A maintainer (amorriscode, contributor) responded on this thread — see the highlighted reply below.

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

  1. 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).
  2. Start a terminal CLI session on the host. Send a prompt. The hook fires.
  3. Start a session on the same host from the desktop app (remote/SSH). Send a prompt. The hook does not fire.
  4. find ~/.claude/remote/plugins -name hooks.json returns nothing, while the plugin cache copy contains the file.
  5. 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)

View original on GitHub ↗

3 Comments

amorriscode contributor · 2 months ago

We're doing a re-write of SSH and will make sure this gets resolved at the same time.

keez97 · 10 days ago

Still happening on desktop 2.19675.0, which runs ccd-cli/2.1.286 on a Debian host. No copy under ~/.claude/remote/plugins/<id>/ has a hooks/ folder, for any plugin. Each copy loads as <name>@inline, so it replaces the copy installed on the host, and that installed copy has its hooks.

If the plugin is also installed on the host, there is a workaround that keeps the hooks in the plugin. Turn the synced copy off in the host's ~/.claude/settings.json:

"enabledPlugins": { "<name>@<marketplace>": true, "<name>@inline": false }

Then run /reload-plugins in running sessions. The installed copy loads with its hooks. That includes an async + asyncRewake Stop hook, which woke an idle desktop session within two seconds of the work it watches for. This is the documented "Keep a session-only plugin from loading" rule: https://code.claude.com/docs/en/plugins/loading#keep-a-session-only-plugin-from-loading

The setting also hides any --plugin-dir copy of that plugin you pass yourself. --settings '{"enabledPlugins":{"<name>@inline":true}}' turns it back on for one command.

The docs disagree about this. https://claude.com/docs/plugins/platform-support says hooks/hooks.json loads in the desktop Code tab, and https://code.claude.com/docs/en/desktop says SSH sessions support plugins. Only the third-party page (https://claude.com/docs/third-party/claude-desktop/ssh-remote-sessions) says the hooks folder is not copied.

kgildeme · 9 days ago

Two additions to the comment above (same ccd-cli/2.1.286, RHEL 9 host):

  • Mods are affected too. A mod plugin (hooks/hooks.json with {"modules": ["./register.js"]}) that registers a slash command in session.start silently loses the command. No error is shown.
  • Only hooks/ at the plugin root is dropped. Another synced plugin from the same marketplace (strict: false) was copied as the whole repo, and in that copy mods/<name>/hooks/register.js came through intact. So it is not a file-type filter.

Showing cached comments. Read the full discussion on GitHub ↗