From 1dbed2c01f51feb2a70399e2531de53dde854812 Mon Sep 17 00:00:00 2001 From: Greg Jackson Date: Sat, 1 Aug 2026 19:00:35 +0100 Subject: [PATCH] fix(settings): include command in add-event dedup key (#2382) Fixes #2382. Co-Authored-By: Claude Opus 4.6 --- bin/gstack-settings-hook | 4 +- .../gstack-settings-hook-schema-aware.test.ts | 49 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/bin/gstack-settings-hook b/bin/gstack-settings-hook index d5404c05d..463b3e4c5 100755 --- a/bin/gstack-settings-hook +++ b/bin/gstack-settings-hook @@ -171,8 +171,9 @@ case "$ACTION" in const matchesEntry = (entry) => { const sameMatcher = (entry.matcher || "") === matcher; + const sameCommand = entry.hooks && entry.hooks[0] && entry.hooks[0].command === cmd; const sameSource = entry._gstack_source === source; - return sameMatcher && sameSource; + return sameMatcher && (sameSource || sameCommand); }; let existing = settings.hooks[event].find(matchesEntry); @@ -184,6 +185,7 @@ case "$ACTION" in if (existing) { existing.hooks = [hookEntry]; + existing._gstack_source = source; } else { const newEntry = { _gstack_source: source, hooks: [hookEntry] }; if (matcher) newEntry.matcher = matcher; diff --git a/test/gstack-settings-hook-schema-aware.test.ts b/test/gstack-settings-hook-schema-aware.test.ts index ada8ec40c..5a5f302ec 100644 --- a/test/gstack-settings-hook-schema-aware.test.ts +++ b/test/gstack-settings-hook-schema-aware.test.ts @@ -111,6 +111,55 @@ describe('add-event', () => { expect(s.hooks.PreToolUse[0].hooks[0].command).toBe('/v2'); }); + test('dedup includes command: same (event, matcher, command) with different source updates in place', () => { + run([ + 'add-event', + '--event', 'PostToolUse', + '--matcher', '(AskUserQuestion|mcp__.*__AskUserQuestion)', + '--command', '/abs/path/to/question-log-hook', + '--source', 'source-A', + '--timeout', '5', + ]); + run([ + 'add-event', + '--event', 'PostToolUse', + '--matcher', '(AskUserQuestion|mcp__.*__AskUserQuestion)', + '--command', '/abs/path/to/question-log-hook', + '--source', 'source-B', + '--timeout', '5', + ]); + const s = settings(); + expect(s.hooks.PostToolUse).toHaveLength(1); + expect(s.hooks.PostToolUse[0]._gstack_source).toBe('source-B'); + }); + + test('dedup includes command: untagged entry with same command is updated not duplicated', () => { + fs.writeFileSync( + settingsFile, + JSON.stringify({ + hooks: { + PostToolUse: [ + { + matcher: '(AskUserQuestion|mcp__.*__AskUserQuestion)', + hooks: [{ type: 'command', command: '/abs/path/to/question-log-hook', timeout: 5 }], + }, + ], + }, + }, null, 2), + ); + run([ + 'add-event', + '--event', 'PostToolUse', + '--matcher', '(AskUserQuestion|mcp__.*__AskUserQuestion)', + '--command', '/abs/path/to/question-log-hook', + '--source', 'plan-tune-cathedral', + '--timeout', '5', + ]); + const s = settings(); + expect(s.hooks.PostToolUse).toHaveLength(1); + expect(s.hooks.PostToolUse[0]._gstack_source).toBe('plan-tune-cathedral'); + }); + test('preserves unrelated existing hooks', () => { fs.writeFileSync( settingsFile,