fix(hooks): skip read-guard advisory on Claude Code runtime (#2001)

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tibsfox
2026-04-10 07:50:35 -07:00
committed by GitHub
parent 04e9bd5e76
commit 21d2bd039d
2 changed files with 24 additions and 1 deletions

View File

@@ -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);

View File

@@ -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');
});
});