From 21d2bd039df268ba220c8476b4563a7d679d6ce3 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Fri, 10 Apr 2026 07:50:35 -0700 Subject: [PATCH] fix(hooks): skip read-guard advisory on Claude Code runtime (#2001) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(hooks): skip read-guard advisory on Claude Code runtime (#1984) Claude Code natively enforces read-before-edit at the runtime level, so the gsd-read-guard.js advisory is redundant — it wastes ~80 tokens per Write/Edit call and clutters tool flow with system-reminder noise. Add early exit when CLAUDE_SESSION_ID is set (standard Claude Code session env var). Non-Claude runtimes (OpenCode, Gemini, etc.) that lack native read-before-edit enforcement continue to receive the advisory as before. Closes #1984 Co-Authored-By: Claude Opus 4.6 * fix(hooks): sanitize runHook env to prevent test failures in Claude Code The runHook() test helper now blanks CLAUDE_SESSION_ID so positive-path tests pass even when the test suite runs inside a Claude Code session. The new skip test passes the env var explicitly via envOverrides. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- hooks/gsd-read-guard.js | 5 +++++ tests/read-guard.test.cjs | 20 +++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/hooks/gsd-read-guard.js b/hooks/gsd-read-guard.js index d1fe3b1e8..335a372d3 100644 --- a/hooks/gsd-read-guard.js +++ b/hooks/gsd-read-guard.js @@ -36,6 +36,11 @@ process.stdin.on('end', () => { process.exit(0); } + // Claude Code natively enforces read-before-edit — skip the advisory (#1984) + if (process.env.CLAUDE_SESSION_ID) { + process.exit(0); + } + const filePath = data.tool_input?.file_path || ''; if (!filePath) { process.exit(0); diff --git a/tests/read-guard.test.cjs b/tests/read-guard.test.cjs index 92999d242..55f0df08d 100644 --- a/tests/read-guard.test.cjs +++ b/tests/read-guard.test.cjs @@ -26,14 +26,17 @@ const HOOK_PATH = path.join(__dirname, '..', 'hooks', 'gsd-read-guard.js'); * Run the read guard hook with a given tool input payload. * Returns { exitCode, stdout, stderr }. */ -function runHook(payload) { +function runHook(payload, envOverrides = {}) { const input = JSON.stringify(payload); + // Sanitize CLAUDE_SESSION_ID so positive-path tests work inside Claude Code sessions + const env = { ...process.env, CLAUDE_SESSION_ID: '', ...envOverrides }; try { const stdout = execFileSync(process.execPath, [HOOK_PATH], { input, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], + env, }); return { exitCode: 0, stdout: stdout.trim(), stderr: '' }; } catch (err) { @@ -221,4 +224,19 @@ describe('gsd-read-guard hook', () => { assert.equal(result.exitCode, 0); assert.equal(result.stdout, ''); }); + + // ─── Claude Code runtime skip (#1984) ───────────────────────────────── + + test('skips advisory on Claude Code runtime (CLAUDE_SESSION_ID set)', () => { + const filePath = path.join(tmpDir, 'existing.js'); + fs.writeFileSync(filePath, 'const x = 1;\n'); + + const result = runHook( + { tool_name: 'Edit', tool_input: { file_path: filePath, old_string: 'const x = 1;', new_string: 'const x = 2;' } }, + { CLAUDE_SESSION_ID: 'test-session-123' } + ); + + assert.equal(result.exitCode, 0); + assert.equal(result.stdout, '', 'should produce no output on Claude Code'); + }); });