diff --git a/src/cli/init.ts b/src/cli/init.ts index 0414bb7..62e4d5d 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -55,6 +55,7 @@ const HOOK_SETTINGS = { type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/session-start.js"', timeout: 5, + _managedBy: "openwolf", }, ], }, @@ -67,6 +68,7 @@ const HOOK_SETTINGS = { type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/pre-read.js"', timeout: 5, + _managedBy: "openwolf", }, ], }, @@ -77,6 +79,7 @@ const HOOK_SETTINGS = { type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/pre-write.js"', timeout: 5, + _managedBy: "openwolf", }, ], }, @@ -89,6 +92,7 @@ const HOOK_SETTINGS = { type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/post-read.js"', timeout: 5, + _managedBy: "openwolf", }, ], }, @@ -99,6 +103,7 @@ const HOOK_SETTINGS = { type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/post-write.js"', timeout: 10, + _managedBy: "openwolf", }, ], }, @@ -111,6 +116,7 @@ const HOOK_SETTINGS = { type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/stop.js"', timeout: 10, + _managedBy: "openwolf", }, ], }, @@ -480,17 +486,22 @@ function replaceOpenWolfHooks( if (!merged.hooks) { merged.hooks = {}; } - const hooks = merged.hooks as Record }>>; + const hooks = merged.hooks as Record }>>; for (const [event, newMatchers] of Object.entries(hookSettings.hooks)) { if (!hooks[event]) { hooks[event] = []; } - // Remove any existing OpenWolf hook entries (match by .wolf/hooks/ in command) + // Remove any existing OpenWolf hook entries. Prefer the explicit + // `_managedBy: "openwolf"` tag, fall back to the legacy + // `.wolf/hooks/` substring match so we still clean up entries + // installed by versions before the tag existed. hooks[event] = hooks[event].filter((entry) => { const isOpenWolfHook = entry.hooks?.some( - (h) => h.command && h.command.includes(".wolf/hooks/") + (h) => + h._managedBy === "openwolf" || + (h.command && h.command.includes(".wolf/hooks/")) ); return !isOpenWolfHook; }); diff --git a/src/cli/update.ts b/src/cli/update.ts index 33cf5cd..47a8ac4 100644 --- a/src/cli/update.ts +++ b/src/cli/update.ts @@ -43,18 +43,22 @@ const BACKUP_FILES = [ ...USER_DATA_FILES, ]; +// Every hook entry carries `_managedBy: "openwolf"`. Claude Code's +// settings round-tripper preserves entries with this provenance tag +// across `/effort`, `/config`, and similar rewrites; untagged entries +// get silently dropped. See cytostack/openwolf#31. const HOOK_SETTINGS = { hooks: { - SessionStart: [{ matcher: "", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/session-start.js"', timeout: 5 }] }], + SessionStart: [{ matcher: "", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/session-start.js"', timeout: 5, _managedBy: "openwolf" }] }], PreToolUse: [ - { matcher: "Read", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/pre-read.js"', timeout: 5 }] }, - { matcher: "Write|Edit|MultiEdit", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/pre-write.js"', timeout: 5 }] }, + { matcher: "Read", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/pre-read.js"', timeout: 5, _managedBy: "openwolf" }] }, + { matcher: "Write|Edit|MultiEdit", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/pre-write.js"', timeout: 5, _managedBy: "openwolf" }] }, ], PostToolUse: [ - { matcher: "Read", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/post-read.js"', timeout: 5 }] }, - { matcher: "Write|Edit|MultiEdit", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/post-write.js"', timeout: 10 }] }, + { matcher: "Read", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/post-read.js"', timeout: 5, _managedBy: "openwolf" }] }, + { matcher: "Write|Edit|MultiEdit", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/post-write.js"', timeout: 10, _managedBy: "openwolf" }] }, ], - Stop: [{ matcher: "", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/stop.js"', timeout: 10 }] }], + Stop: [{ matcher: "", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/stop.js"', timeout: 10, _managedBy: "openwolf" }] }], }, }; @@ -358,15 +362,20 @@ function replaceOpenWolfHooks( ): Record { const merged = { ...existing }; if (!merged.hooks) merged.hooks = {}; - const hooks = merged.hooks as Record }>>; + const hooks = merged.hooks as Record }>>; for (const [event, newMatchers] of Object.entries(hookSettings.hooks)) { if (!hooks[event]) hooks[event] = []; - // Remove existing OpenWolf hook entries + // Remove existing OpenWolf hook entries. Prefer the explicit + // `_managedBy: "openwolf"` tag, fall back to the legacy + // `.wolf/hooks/` substring match so we still clean up entries + // installed by versions before the tag existed. hooks[event] = hooks[event].filter((entry) => { const isOpenWolfHook = entry.hooks?.some( - (h) => h.command && h.command.includes(".wolf/hooks/") + (h) => + h._managedBy === "openwolf" || + (h.command && h.command.includes(".wolf/hooks/")) ); return !isOpenWolfHook; });