From c4b313c60f8d84dcb11c30042a7a0a2e768f8506 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Fri, 20 Mar 2026 16:24:06 -0400 Subject: [PATCH 1/2] fix: stale hook detection checks wrong directory path gsd-check-update.js looked for hooks in configDir/hooks/ (e.g., ~/.claude/hooks/) but the installer writes hooks to configDir/get-shit-done/hooks/. This mismatch caused false stale hook warnings that persisted even after updating. Also clears the update cache during install so the next session re-evaluates hook versions with the correct path. Closes #1249 Co-Authored-By: Claude Opus 4.6 --- bin/install.js | 5 +++++ hooks/gsd-check-update.js | 3 ++- tests/core.test.cjs | 20 ++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/bin/install.js b/bin/install.js index 674120eb4..4f8eeced7 100755 --- a/bin/install.js +++ b/bin/install.js @@ -3900,6 +3900,11 @@ function install(isGlobal, runtime = 'claude') { } } + // Clear stale update cache so next session re-evaluates hook versions + // targetDir is e.g. ~/.claude/get-shit-done/, parent is the config dir + const updateCacheFile = path.join(path.dirname(targetDir), 'cache', 'gsd-update-check.json'); + try { fs.unlinkSync(updateCacheFile); } catch (e) { /* cache may not exist yet */ } + if (failures.length > 0) { console.error(`\n ${yellow}Installation incomplete!${reset} Failed: ${failures.join(', ')}`); process.exit(1); diff --git a/hooks/gsd-check-update.js b/hooks/gsd-check-update.js index 9076ec038..1b7b27ed3 100755 --- a/hooks/gsd-check-update.js +++ b/hooks/gsd-check-update.js @@ -65,9 +65,10 @@ const child = spawn(process.execPath, ['-e', ` } catch (e) {} // Check for stale hooks — compare hook version headers against installed VERSION + // Hooks live inside get-shit-done/hooks/, not configDir/hooks/ let staleHooks = []; if (configDir) { - const hooksDir = path.join(configDir, 'hooks'); + const hooksDir = path.join(configDir, 'get-shit-done', 'hooks'); try { if (fs.existsSync(hooksDir)) { const hookFiles = fs.readdirSync(hooksDir).filter(f => f.startsWith('gsd-') && f.endsWith('.js')); diff --git a/tests/core.test.cjs b/tests/core.test.cjs index e43f53f9f..251793d46 100644 --- a/tests/core.test.cjs +++ b/tests/core.test.cjs @@ -967,6 +967,26 @@ describe('stale hook filter', () => { }); }); +// ─── stale hook path regression (#1249) ────────────────────────────────────── + +describe('stale hook path', () => { + test('gsd-check-update.js checks get-shit-done/hooks/ not configDir/hooks/', () => { + const content = fs.readFileSync( + path.join(__dirname, '..', 'hooks', 'gsd-check-update.js'), 'utf-8' + ); + assert.ok( + content.includes("path.join(configDir, 'get-shit-done', 'hooks')"), + 'stale hook check must look in configDir/get-shit-done/hooks/, not configDir/hooks/' + ); + assert.ok( + !content.includes("path.join(configDir, 'hooks')") || + content.indexOf("path.join(configDir, 'get-shit-done', 'hooks')") < + content.indexOf("path.join(configDir, 'hooks')") + 100, // allow the old pattern only if corrected version exists first + 'should not use the wrong hooks path' + ); + }); +}); + // ─── resolveWorktreeRoot ───────────────────────────────────────────────────── describe('resolveWorktreeRoot', () => { From 57cf0bd97bd6ecc130245b8a4e09d79051446b5a Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Fri, 20 Mar 2026 16:28:15 -0400 Subject: [PATCH 2/2] enhance: add 'Follow the Indirection' debugging technique to gsd-debugger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Teaches the debugger agent to trace path/URL/key construction across producer and consumer code — prevents shallow investigation that misses directory mismatches like the stale hooks bug (#1249). Co-Authored-By: Claude Opus 4.6 --- agents/gsd-debugger.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/agents/gsd-debugger.md b/agents/gsd-debugger.md index 6f1f4239f..8c7109032 100644 --- a/agents/gsd-debugger.md +++ b/agents/gsd-debugger.md @@ -409,6 +409,39 @@ git bisect bad # or good, based on testing 100 commits between working and broken: ~7 tests to find exact breaking commit. +## Follow the Indirection + +**When:** Code constructs paths, URLs, keys, or references from variables — and the constructed value might not point where you expect. + +**The trap:** You read code that builds a path like `path.join(configDir, 'hooks')` and assume it's correct because it looks reasonable. But you never verified that the constructed path matches where another part of the system actually writes/reads. + +**How:** +1. Find the code that **produces** the value (writer/installer/creator) +2. Find the code that **consumes** the value (reader/checker/validator) +3. Trace the actual resolved value in both — do they agree? +4. Check every variable in the path construction — where does each come from? What's its actual value at runtime? + +**Common indirection bugs:** +- Path A writes to `dir/sub/hooks/` but Path B checks `dir/hooks/` (directory mismatch) +- Config value comes from cache/template that wasn't updated +- Variable is derived differently in two places (e.g., one adds a subdirectory, the other doesn't) +- Template placeholder (`{{VERSION}}`) not substituted in all code paths + +**Example:** Stale hook warning persists after update +``` +Check code says: hooksDir = path.join(configDir, 'hooks') + configDir = ~/.claude + → checks ~/.claude/hooks/ + +Installer says: hooksDest = path.join(targetDir, 'hooks') + targetDir = ~/.claude/get-shit-done + → writes to ~/.claude/get-shit-done/hooks/ + +MISMATCH: Checker looks in wrong directory → hooks "not found" → reported as stale +``` + +**The discipline:** Never assume a constructed path is correct. Resolve it to its actual value and verify the other side agrees. When two systems share a resource (file, directory, key), trace the full path in both. + ## Technique Selection | Situation | Technique | @@ -419,6 +452,7 @@ git bisect bad # or good, based on testing | Know the desired output | Working backwards | | Used to work, now doesn't | Differential debugging, Git bisect | | Many possible causes | Comment out everything, Binary search | +| Paths, URLs, keys constructed from variables | Follow the indirection | | Always | Observability first (before making changes) | ## Combining Techniques