From 5a0880bf3550b3eeef7777e3abb59f11678d989c Mon Sep 17 00:00:00 2001 From: ManniX-ITA <20623405+mann1x@users.noreply.github.com> Date: Fri, 1 May 2026 13:39:50 +0200 Subject: [PATCH] init/update: tag hook entries with _managedBy: "openwolf" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `_managedBy: "openwolf"` to every hook object in `HOOK_SETTINGS` so Claude Code's settings round-tripper recognizes them as third-party managed entries and preserves them through `/effort`, `/config`, and similar rewrites. Without the tag, entries get silently dropped: a working OpenWolf install can be de-wired by typing `/effort medium` once, since Claude Code's merge logic only preserves entries it recognizes as owned (claude-hooks uses the same field, for example). Also tightens `replaceOpenWolfHooks` to recognize the new tag in addition to the legacy `.wolf/hooks/` substring — defensive against future path schema changes, and keeps the dedupe correct for installs upgrading from a pre-tag version. The two changes are minimal and backward-compatible: untagged entries from older installs still match the substring fallback, so upgrades clean up cleanly. New installs get tagged from the start. Fixes #31. --- src/cli/init.ts | 17 ++++++++++++++--- src/cli/update.ts | 27 ++++++++++++++++++--------- 2 files changed, 32 insertions(+), 12 deletions(-) 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; });