Cowork cloud sessions sync a plugin's hooks/hooks.json but never register it — enabledPlugins is null in launcher-settings.json, so plugin hooks silently never fire

Status Fixed / completed
Maintainer reply None cached
Activity 2 comments · opened Sep 10, 2026 · closed Sep 10, 2026

This concerns Cowork on claude.ai (cloud sessions, CLAUDE_CODE_ENTRYPOINT=remote_cowork_trigger), not the CLI, so the CLI-only required fields below are not applicable. Reproduced 10 Sept 2026 from inside a running cloud session.

Summary

A plugin that contains hooks is synced into the cloud container with its hooks/hooks.json and its scripts intact, is reported as enabled: true by every surface that reports plugin state — and none of its hooks ever run. There is no error, no log line, and no way for the plugin author to detect it from inside the plugin. The plugin appears to be working.

Evidence, all read inside one cloud session

1. The account says the plugin is enabled, with its hooks parsed.

GET /api/organizations/{org}/plugins/list-plugins?enabled_only=true returns the plugin with:

name: answering-aviv-gate
enabled: true
presents_as: null
hooks: [ UserPromptSubmit, Stop, PreToolUse ]   # 3 entries, matchers and commands intact
installation_preference: "available"

2. ListPlugins inside the session agrees.

{"id":"plugin_…","name":"answering-aviv-gate","enabled":true}

3. The plugin is on disk in the container, hooks and all.

/root/.claude/plugins/synced/<org>_<account>/manifest.json
/root/.claude/plugins/synced/<org>_<account>/answering-aviv-gate/hooks/hooks.json
/root/.claude/plugins/synced/<org>_<account>/answering-aviv-gate/hooks/scripts/connector-gate.py
/root/.claude/plugins/synced/<org>_<account>/answering-aviv-gate/hooks/scripts/signoff-gate.py
/root/.claude/plugins/synced/<org>_<account>/answering-aviv-gate/hooks/scripts/ensure-answering-aviv.py

hooks.json declares UserPromptSubmit, Stop, and PreToolUse with matcher: "mcp__.*", each a command hook invoking python3 ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/….

4. Nothing registers them. ~/.claude/settings.json and ~/.claude/settings.local.json do not exist. The only settings file present is ~/.claude/launcher-settings.json, written by the environment manager at session start, and it contains:

{
  "hooks": {
    "SessionStart": [{"hooks":[{"command":"~/.claude/session-start-git-identity.sh","type":"command"}]}],
    "Stop":        [{"hooks":[{"command":"~/.claude/stop-hook-git-check.sh","type":"command"}],"matcher":""}]
  },
  "enabledPlugins": null
}

Only the platform's own hooks are registered. enabledPlugins is null, and no entry from the synced plugin's hooks.json appears anywhere.

5. Live proof the PreToolUse hook does not fire. The plugin's PreToolUse matcher is mcp__.* and its script denies an MCP call whose owning skill has not been loaded in the session. In a session where that skill had not been loaded, mcp__Gmail_Multi__list_accounts was called and returned normally. Replaying the same tool name and transcript through the script by hand returns the deny payload, so the script is correct and simply was never invoked.

Expected

Either the synced plugin's hooks are merged into the effective hook config for the session (equivalently, enabledPlugins reflects the plugin the platform just synced), or the session surfaces somewhere that the plugin's hook components are inactive.

Actual

Hooks are synced, reported enabled by two independent APIs, and silently never registered. Every guard the plugin exists to enforce is dead, and the plugin author's only way to discover this is to deliberately trigger a deny and notice it did not happen.

Why this is worse than a normal no-op

Hooks are the one plugin component that exists to be a hard guard — the thing you reach for precisely because a prompt-level rule was not reliable. A hook that silently does not run leaves the operator believing a class of mistake is now impossible when nothing at all is checking. Skills and commands from the same account sync and work correctly in these sessions; only hooks are dropped.

Notes on installation_preference

Every plugin on this account reports installation_preference: "available", including the ~46 skill-presenting plugins that do work in these sessions, so it does not look like the discriminator. There also appears to be no write route for it: /api/organizations/{org}/plugins/{plugin_id} answers Allow: DELETE on anything but GET, and POST/PUT/PATCH on every install/enable/preference path tried returned 404 or 405. If a per-account switch is meant to make plugin hooks active in cloud sessions, it is not reachable from the documented surface or from the claude.ai API the plugins settings screen itself uses.

Related, none of which is this

  • #83643 — desktop remote sessions: plugin sync omits hooks/ entirely. Here hooks/ is present and complete; the gap is registration.
  • #75972 — plugin hooks.json never fires while the plugin shows installed and enabled, on local CLI. Same symptom, different runtime; this one adds the cloud-session settings file showing enabledPlugins: null.
  • #87657 — Agent SDK / desktop-app sessions load 0 hooks despite settings declaring them.
  • #16288, #88650 — plugin hook loading from external hooks.json.

Environment

  • claude.ai Cowork, cloud session (no desktop bridge), model claude-opus-5
  • Container: Linux 6.18.44-fc-v24, ~/.claude/launcher-settings.json present, no settings.json
  • Plugin delivered through a personal marketplace ("My Uploads"), version 0.4.0

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗