mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
* fix(hooks): detect Claude Code via stdin session_id, not filtered env (#2520) The #2344 fix assumed `CLAUDECODE` would propagate to hook subprocesses. On Claude Code v2.1.116 it doesn't — Claude Code applies a separate env filter to PreToolUse hook commands that drops bare CLAUDECODE and CLAUDE_SESSION_ID, keeping only CLAUDE_CODE_*-prefixed vars plus CLAUDE_PROJECT_DIR. As a result every Edit/Write on an existing file produced a redundant READ-BEFORE-EDIT advisory inside Claude Code. Use `data.session_id` from the hook's stdin JSON as the primary Claude Code signal (it's part of Claude Code's documented PreToolUse hook-input schema). Keep CLAUDE_CODE_ENTRYPOINT / CLAUDE_CODE_SSE_PORT env checks as propagation-verified fallbacks, and keep the legacy CLAUDE_SESSION_ID / CLAUDECODE checks for back-compat and future-proofing. Add tests/bug-2520-read-guard-hook-subprocess-env.test.cjs, which spawns the hook with an env mirroring the actual Claude Code hook-subprocess filter. Extend the legacy test harnesses to also strip the propagation-verified CLAUDE_CODE_* vars so positive-path tests keep passing when the suite itself runs inside a Claude Code session (same class of leak as #2370 / PR #2375, now covering the new detection signals). Non-Claude-host behavior (OpenCode / MiniMax) is unchanged: with no `session_id` on stdin and no CLAUDE_CODE_* env var, the advisory still fires. Closes #2520 * test(2520): isolate session_id signal from env fallbacks in regression test Per reviewer feedback (Copilot + CodeRabbit on #2521): the session_id isolation test used the helper's default CLAUDE_CODE_ENTRYPOINT / CLAUDE_CODE_SSE_PORT values, so the env fallback would rescue the skip even if the primary `data.session_id` check regressed. Pass an explicit env override that clears those fallbacks, so only the stdin `session_id` signal can trigger the skip. Other cases (env-only fallback, negative / non-Claude host) already override env appropriately. --------- Co-authored-by: forfrossen <forfrossensvart@gmail.com>
102 lines
3.8 KiB
JavaScript
102 lines
3.8 KiB
JavaScript
#!/usr/bin/env node
|
|
// gsd-hook-version: {{GSD_VERSION}}
|
|
// GSD Read Guard — PreToolUse hook
|
|
// Injects advisory guidance when Write/Edit targets an existing file,
|
|
// reminding the model to Read the file first.
|
|
//
|
|
// Background: Non-Claude models (e.g. MiniMax M2.5 on OpenCode) don't
|
|
// natively follow the read-before-edit pattern. When they attempt to
|
|
// Write/Edit an existing file without reading it, the runtime rejects
|
|
// with "You must read file before overwriting it." The model retries
|
|
// without reading, creating an infinite loop that burns through usage.
|
|
//
|
|
// This hook prevents that loop by injecting clear guidance BEFORE the
|
|
// tool call reaches the runtime. The model sees the advisory and can
|
|
// issue a Read call on the next turn.
|
|
//
|
|
// Triggers on: Write and Edit tool calls
|
|
// Action: Advisory (does not block) — injects read-first guidance
|
|
// Only fires when the target file already exists on disk.
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
let input = '';
|
|
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
|
|
process.stdin.setEncoding('utf8');
|
|
process.stdin.on('data', chunk => input += chunk);
|
|
process.stdin.on('end', () => {
|
|
clearTimeout(stdinTimeout);
|
|
try {
|
|
const data = JSON.parse(input);
|
|
const toolName = data.tool_name;
|
|
|
|
// Only intercept Write and Edit tool calls
|
|
if (toolName !== 'Write' && toolName !== 'Edit') {
|
|
process.exit(0);
|
|
}
|
|
|
|
// Claude Code natively enforces read-before-edit — skip the advisory (#1984, #2344, #2520).
|
|
//
|
|
// Detection signals, in priority order:
|
|
// 1. `data.session_id` on the hook's stdin payload — part of Claude
|
|
// Code's documented PreToolUse hook-input schema, always present.
|
|
// Reliable across Claude Code versions because it's schema, not env.
|
|
// 2. `CLAUDE_CODE_ENTRYPOINT` / `CLAUDE_CODE_SSE_PORT` — env vars that
|
|
// Claude Code does propagate to hook subprocesses (verified on
|
|
// Claude Code CLI 2.1.116).
|
|
// 3. `CLAUDE_SESSION_ID` / `CLAUDECODE` — kept for back-compat and in
|
|
// case future Claude Code versions propagate them to hook
|
|
// subprocesses. On 2.1.116 they reach Bash tool subprocesses but
|
|
// not hook subprocesses, which is why checking them alone is
|
|
// insufficient (regression of #2344 fixed here as #2520).
|
|
const isClaudeCode =
|
|
(typeof data.session_id === 'string' && data.session_id.length > 0) ||
|
|
process.env.CLAUDE_CODE_ENTRYPOINT ||
|
|
process.env.CLAUDE_CODE_SSE_PORT ||
|
|
process.env.CLAUDE_SESSION_ID ||
|
|
process.env.CLAUDECODE;
|
|
if (isClaudeCode) {
|
|
process.exit(0);
|
|
}
|
|
|
|
const filePath = data.tool_input?.file_path || '';
|
|
if (!filePath) {
|
|
process.exit(0);
|
|
}
|
|
|
|
// Only inject guidance when the file already exists.
|
|
// New files don't need a prior Read — the runtime allows creating them directly.
|
|
let fileExists = false;
|
|
try {
|
|
fs.accessSync(filePath, fs.constants.F_OK);
|
|
fileExists = true;
|
|
} catch {
|
|
// File does not exist — no guidance needed
|
|
}
|
|
|
|
if (!fileExists) {
|
|
process.exit(0);
|
|
}
|
|
|
|
const fileName = path.basename(filePath);
|
|
|
|
// Advisory guidance — does not block the operation
|
|
const output = {
|
|
hookSpecificOutput: {
|
|
hookEventName: 'PreToolUse',
|
|
additionalContext:
|
|
`READ-BEFORE-EDIT REMINDER: You are about to modify "${fileName}" which already exists. ` +
|
|
'If you have not already used the Read tool to read this file in the current session, ' +
|
|
'you MUST Read it first before editing. The runtime will reject edits to files that ' +
|
|
'have not been read. Use the Read tool on this file path, then retry your edit.',
|
|
},
|
|
};
|
|
|
|
process.stdout.write(JSON.stringify(output));
|
|
} catch {
|
|
// Silent fail — never block tool execution
|
|
process.exit(0);
|
|
}
|
|
});
|