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:
@@ -187,11 +187,37 @@ function releaseLock(rt = runtimeDir()) {
|
||||
|
||||
/* --------------------------------------------------------- legacy cleanup */
|
||||
|
||||
/**
|
||||
* `claude plugin install` materializes the plugin's inline hooks into
|
||||
* `~/.claude/settings.json` itself, with `${CLAUDE_PLUGIN_ROOT}` already
|
||||
* resolved to the cache path — confirmed by installing this plugin for real
|
||||
* and inspecting the file. Those entries also contain `hook-handler.js`, so
|
||||
* `isOurEntry()` alone (checkout vs. plugin, both match the same substring)
|
||||
* cannot tell a legitimate plugin-installed hook from a leftover
|
||||
* `npm run install-hooks` entry. Only entries whose command does NOT resolve
|
||||
* under the plugin cache are the legacy, checkout-installed kind.
|
||||
*
|
||||
* @returns {(entry: object) => boolean}
|
||||
*/
|
||||
function isCheckoutHookEntry(claudeHome = getClaudeHome()) {
|
||||
const cacheRoot = path.join(claudeHome, "plugins", "cache");
|
||||
return (entry) => {
|
||||
if (!isOurEntry(entry)) return false;
|
||||
const commands = [
|
||||
entry.command,
|
||||
...(Array.isArray(entry.hooks) ? entry.hooks.map((h) => h.command) : []),
|
||||
];
|
||||
return !commands.some((c) => typeof c === "string" && c.includes(cacheRoot));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove hook entries a previous `npm run install-hooks` wrote into
|
||||
* ~/.claude/settings.json. The plugin installs its own hooks, and events carry
|
||||
* no id — two handlers mean every token and cost figure is counted twice.
|
||||
* The original file is copied aside before any write.
|
||||
* Never touches the plugin's own cache-resolved entries (see
|
||||
* `isCheckoutHookEntry`) — only genuine leftover checkout paths. The original
|
||||
* file is copied aside before any write.
|
||||
*
|
||||
* @returns {number} how many entries were removed
|
||||
*/
|
||||
@@ -204,10 +230,11 @@ function stripLegacyHooks(settingsPath = getSettingsPath()) {
|
||||
}
|
||||
if (!settings.hooks || typeof settings.hooks !== "object") return 0;
|
||||
|
||||
const isLegacy = isCheckoutHookEntry();
|
||||
let removed = 0;
|
||||
for (const [type, entries] of Object.entries(settings.hooks)) {
|
||||
if (!Array.isArray(entries)) continue;
|
||||
const kept = entries.filter((e) => !isOurEntry(e));
|
||||
const kept = entries.filter((e) => !isLegacy(e));
|
||||
removed += entries.length - kept.length;
|
||||
if (kept.length) settings.hooks[type] = kept;
|
||||
else delete settings.hooks[type];
|
||||
@@ -506,6 +533,7 @@ module.exports = {
|
||||
lockIsStale,
|
||||
acquireLock,
|
||||
releaseLock,
|
||||
isCheckoutHookEntry,
|
||||
stripLegacyHooks,
|
||||
linkCli,
|
||||
installDeps,
|
||||
|
||||
@@ -14,7 +14,6 @@ const os = require("os");
|
||||
const path = require("path");
|
||||
|
||||
const boot = require("./plugin-bootstrap");
|
||||
const { isOurEntry } = require("./install-hooks");
|
||||
const { getSettingsPath, getDataDir } = require("../server/lib/claude-home");
|
||||
const { mcpBuildStatus } = require("./check-mcp-build");
|
||||
|
||||
@@ -104,9 +103,10 @@ function countLegacyHookEntries() {
|
||||
try {
|
||||
const settings = JSON.parse(fs.readFileSync(getSettingsPath(), "utf8"));
|
||||
if (!settings.hooks) return 0;
|
||||
const isLegacy = boot.isCheckoutHookEntry();
|
||||
return Object.values(settings.hooks)
|
||||
.filter(Array.isArray)
|
||||
.reduce((n, entries) => n + entries.filter(isOurEntry).length, 0);
|
||||
.reduce((n, entries) => n + entries.filter(isLegacy).length, 0);
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user