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
+43
View File
@@ -192,6 +192,49 @@ describe("legacy hook cleanup", () => {
assert.ok(fs.existsSync(`${settings}.ccam-bak`), "must back up before writing");
});
it("never removes the plugin's own cache-resolved hook entries", () => {
// Regression: `claude plugin install` materializes the plugin's inline
// hooks into settings.json itself, with ${CLAUDE_PLUGIN_ROOT} resolved to
// the cache path — confirmed against a real install. Those entries also
// contain "hook-handler.js", so a naive substring check would strip them
// right along with genuine checkout leftovers, killing the plugin's own
// hooks on every session start.
const cachePath = path.join(
TMP_HOME,
"plugins",
"cache",
"claude-code-agent-monitor-plugins",
"ccam",
"abc123",
"scripts",
"hook-handler.js"
);
fs.writeFileSync(
settings,
JSON.stringify({
hooks: {
PreToolUse: [
{
matcher: "*",
hooks: [{ type: "command", command: `node "${cachePath}" PreToolUse` }],
},
{
matcher: "*",
hooks: [
{ type: "command", command: "node /repo/scripts/hook-handler.js PreToolUse" },
],
},
],
},
})
);
const removed = boot.stripLegacyHooks(settings);
assert.equal(removed, 1);
const after = JSON.parse(fs.readFileSync(settings, "utf8"));
assert.equal(after.hooks.PreToolUse.length, 1);
assert.match(JSON.stringify(after.hooks.PreToolUse), /plugins.*cache/);
});
it("leaves a file with no CCAM hooks untouched and writes no backup", () => {
fs.writeFileSync(settings, JSON.stringify({ hooks: { Stop: [{ hooks: [] }] } }));
assert.equal(boot.stripLegacyHooks(settings), 0);