fix(plugins): stop stripLegacyHooks from deleting the plugin's own hooks

claude plugin install materializes the plugin's inline hooks into
~/.claude/settings.json itself, with \${CLAUDE_PLUGIN_ROOT} resolved to the
actual cache path — confirmed by installing the plugin for real and
inspecting the file. Those entries also contain "hook-handler.js", so
isOurEntry()'s plain substring match could not tell a legitimate
plugin-installed hook from a leftover npm run install-hooks entry: every
SessionStart would have stripped the plugin's own working hooks right back
out. isCheckoutHookEntry() only removes entries whose command does NOT
resolve under ~/.claude/plugins/cache/. plugin-doctor.js's duplicate-hook
count uses the same predicate.
This commit is contained in:
2026-08-10 16:28:59 +07:00
parent a65ee1512e
commit 205f40c29c
5 changed files with 103 additions and 17 deletions
+19 -9
View File
@@ -1121,17 +1121,27 @@ flowchart TD
### Plugin installs declare the same hooks instead
When the dashboard is installed as the `ccam` Claude Code plugin, the eight hook
entries come from the inline `hooks` block in `.claude-plugin/plugin.json`
(each running `${CLAUDE_PLUGIN_ROOT}/scripts/hook-handler.js`), and
`install-hooks.js` is not used at all. Both at once would POST every event twice
— events carry no id, so ingest cannot deduplicate them, and every token and
cost figure would double. Two guards keep that from happening silently:
entries are declared inline in `.claude-plugin/plugin.json` (each running
`${CLAUDE_PLUGIN_ROOT}/scripts/hook-handler.js`), and `install-hooks.js` is not
used at all. `claude plugin install` itself materializes these into
`~/.claude/settings.json` with `${CLAUDE_PLUGIN_ROOT}` already resolved to the
plugin's cache path — confirmed by installing the plugin for real and
inspecting the file, not just reading the docs. Both a checkout install and a
plugin install writing hooks at once would POST every event twice — events
carry no id, so ingest cannot deduplicate them, and every token and cost figure
would double. Two guards keep that from happening silently:
- `scripts/plugin-bootstrap.js` strips `hook-handler.js` entries out of
`~/.claude/settings.json` on session start (backing the file up as
`settings.json.ccam-bak` first) and logs what it removed.
- `scripts/plugin-bootstrap.js` strips checkout-style `hook-handler.js` entries
out of `~/.claude/settings.json` on session start (backing the file up as
`settings.json.ccam-bak` first) and logs what it removed. Because the
plugin's own entries ALSO contain `hook-handler.js` (just resolved to a cache
path instead of a raw filesystem one), a plain substring match cannot tell
them apart — `isCheckoutHookEntry()` only treats an entry as removable when
its command does NOT resolve under `~/.claude/plugins/cache/`, so the
plugin's own legitimate hooks are never touched.
- `install-hooks.js` warns when the plugin runtime state exists, and
`/ccam-doctor` reports any surviving duplicates as a `FAIL`.
`/ccam-doctor` reports any surviving checkout-style duplicates as a `FAIL`
using the same `isCheckoutHookEntry()` predicate.
`scripts/plugin-bootstrap.js` also owns the rest of the plugin's runtime: the
Node >= 22.5 gate (`node:sqlite`), an atomic `mkdir` lock, the dependency