From d7b613d147e95d7e856d28a202cc198d510b46b0 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Fri, 17 Apr 2026 09:30:20 -0400 Subject: [PATCH] fix(hooks): check CLAUDECODE env var in read-guard skip (#2344) (#2352) Closes #2344 Co-authored-by: Claude Sonnet 4.6 --- hooks/gsd-read-guard.js | 4 +- ...ug-2344-read-guard-claudecode-env.test.cjs | 98 +++++++++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 tests/bug-2344-read-guard-claudecode-env.test.cjs diff --git a/hooks/gsd-read-guard.js b/hooks/gsd-read-guard.js index 335a372d3..1ec61cd01 100644 --- a/hooks/gsd-read-guard.js +++ b/hooks/gsd-read-guard.js @@ -36,8 +36,8 @@ process.stdin.on('end', () => { process.exit(0); } - // Claude Code natively enforces read-before-edit — skip the advisory (#1984) - if (process.env.CLAUDE_SESSION_ID) { + // Claude Code natively enforces read-before-edit — skip the advisory (#1984, #2344) + if (process.env.CLAUDE_SESSION_ID || process.env.CLAUDECODE) { process.exit(0); } diff --git a/tests/bug-2344-read-guard-claudecode-env.test.cjs b/tests/bug-2344-read-guard-claudecode-env.test.cjs new file mode 100644 index 000000000..c38596933 --- /dev/null +++ b/tests/bug-2344-read-guard-claudecode-env.test.cjs @@ -0,0 +1,98 @@ +/** + * Regression test for bug #2344 + * + * gsd-read-guard.js checked process.env.CLAUDE_SESSION_ID to detect the + * Claude Code runtime and skip its advisory. However, Claude Code CLI exports + * CLAUDECODE=1, not CLAUDE_SESSION_ID. The skip never fired, so the + * READ-BEFORE-EDIT advisory injected on every Edit/Write call inside Claude + * Code — producing noise in long-running sessions. + * + * Fix: check CLAUDECODE (and CLAUDE_SESSION_ID for back-compat) before + * emitting the advisory. + */ + +process.env.GSD_TEST_MODE = '1'; + +const { test, describe, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { execFileSync } = require('node:child_process'); + +const { createTempDir, cleanup } = require('./helpers.cjs'); + +const HOOK_PATH = path.join(__dirname, '..', 'hooks', 'gsd-read-guard.js'); + +function runHook(payload, envOverrides = {}) { + const input = JSON.stringify(payload); + const env = { + ...process.env, + CLAUDE_SESSION_ID: '', + CLAUDECODE: '', + ...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) { + return { + exitCode: err.status ?? 1, + stdout: (err.stdout || '').toString().trim(), + stderr: (err.stderr || '').toString().trim(), + }; + } +} + +describe('bug #2344: read guard skips on CLAUDECODE env var', () => { + let tmpDir; + + beforeEach(() => { tmpDir = createTempDir('gsd-read-guard-2344-'); }); + afterEach(() => { cleanup(tmpDir); }); + + test('skips advisory when CLAUDECODE=1 is set (Claude Code CLI env)', () => { + 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;' } }, + { CLAUDECODE: '1' } + ); + + assert.equal(result.exitCode, 0); + assert.equal(result.stdout, '', 'advisory must not fire when CLAUDECODE=1'); + }); + + test('skips advisory when CLAUDE_SESSION_ID is set (back-compat)', () => { + 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, '', 'advisory must not fire when CLAUDE_SESSION_ID is set'); + }); + + test('still injects advisory when neither CLAUDECODE nor CLAUDE_SESSION_ID is 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;' } }, + { CLAUDECODE: '', CLAUDE_SESSION_ID: '' } + ); + + assert.equal(result.exitCode, 0); + assert.ok(result.stdout.length > 0, 'advisory should fire on non-Claude-Code runtimes'); + const output = JSON.parse(result.stdout); + assert.ok(output.hookSpecificOutput?.additionalContext?.includes('Read')); + }); +});