Plugin hooks never run when the plugin is installed from a directory-source marketplace
Summary
A plugin installed from a directory-source marketplace reports Status: ✔ enabled,
ships its hooks/hooks.json, and populates its versioned cache dir — but its hooks never
run. The same plugin at the same version installed from a github-source marketplace
runs them normally.
Nothing on screen indicates the difference, so the standard local plugin-development loop
(add your repo as a directory marketplace → install → test) silently exercises a build with
no hooks.
Environment
- Claude Code 2.1.233
- Container:
node:22-bookworm-slim, linux/arm64 (Docker Desktop 29.1.3 on macOS) - Node 22.23.2
- Plugin under test:
cli-dispatchv4.18.0 (https://github.com/rbinar/cli-dispatch) - Auth: API-key style (
ANTHROPIC_BASE_URL+ANTHROPIC_AUTH_TOKEN)
Reproduction
The plugin registers a SessionStart hook:
{
"hooks": {
"SessionStart": [
{ "matcher": "startup", "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/policy-inject.mjs\"" }] }
]
}
}
For the test, that script is replaced with a shim that appends to a canary file. The shim
is self-tested by running it directly before every session, so a broken shim can never be
mistaken for a hook that did not fire:
#!/usr/bin/env node
import { appendFileSync } from "node:fs"
try { appendFileSync("/home/dev/CANARY.txt", "FIRED\n") } catch {}
process.exit(0)
A — github source → hook runs
claude plugin marketplace add rbinar/cli-dispatch
claude plugin install cli-dispatch@cli-dispatch -y
# install the canary shim over scripts/policy-inject.mjs, verify it writes when run directly
rm -f /home/dev/CANARY.txt
claude -p "say OK"
ls /home/dev/CANARY.txt # present
Result: FIRED on 2/2 runs.
B — directory source → hook does not run
Same container, same session state, same plugin version, freshly cleared plugin cache:
claude plugin marketplace remove cli-dispatch
rm -rf ~/.claude/plugins/cache/cli-dispatch
claude plugin marketplace add /home/dev/cli-dispatch-repo # local clone, checked out at v4.18.0
claude plugin install cli-dispatch@cli-dispatch -y
# same canary shim, same self-test
rm -f /home/dev/CANARY.txt
claude -p "say OK"
ls /home/dev/CANARY.txt # absent
Result: ABSENT on 2/2 runs.
Expected vs actual
Expected: a plugin's hooks run whenever the plugin is installed and enabled, regardless
of how its marketplace was sourced.
Actual: hooks run only for github-sourced marketplaces. Directory-sourced installs are
indistinguishable in every surface I could inspect, yet their hooks are inert.
What I ruled out
Each of these was tested and is not the cause:
- Print vs interactive mode. Reproduced in both
claude -pand a real interactive
session under a pty (script -qec claude /dev/null) with onboarding pre-cleared.
- Directory trust. Setting
hasTrustDialogAccepted: truefor the working directory in
~/.claude.json changed nothing.
- Plugin version. Pinned both arms to v4.18.0 (
git checkout v4.18.0for the directory
arm). Also seen with v4.20.0 on the directory arm.
- Hook command interpreter. Replacing
node …with a plain/bin/bash -c 'echo … >> file'
hook in the plugin's hooks.json also did not fire under a directory source.
- Plugin disabled / not installed.
claude plugin listreports
Status: ✔ enabled in both arms; installed_plugins.json has the correct installPath
and version; hooks/hooks.json is present in the cache dir in both arms.
SessionStartnot firing at all. ASessionStarthook registered in
~/.claude/settings.json (not via a plugin) does fire in the very same session where
the plugin's hook does not — so the event itself is delivered.
- A broken canary shim. Every reported run self-tested the shim first. (One earlier run
of mine did produce a false negative from a shim with a syntax error; that run was
discarded and is not part of the results above.)
Impact
Plugin authors developing locally add their working tree as a directory marketplace — it is
the only way to test uncommitted changes. In that loop, hooks are silently dead: aSessionStart hook can appear correct, be enabled, and never execute, with no diagnostic.
Any hook behavior verified this way is unverified in practice.
A warning at install time, or in claude plugin list, would be enough if this is intended
rather than a bug.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗