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:
+19
-9
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user