fix(hooks): add read-before-edit guard for non-Claude runtimes (#1645)
* fix(hooks): add read-before-edit guidance for non-Claude runtimes When models that don't natively enforce read-before-edit hit the guard, the error message now includes explicit instruction to Read first. This prevents infinite retry loops that burn through usage. Closes #1628 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(build): register gsd-read-guard.js in HOOKS_TO_COPY and harden tests The hook was missing from scripts/build-hooks.js, so global installs would never receive the hook file in hooks/dist/. Also adds tests for build registration, install uninstall list, and non-string file_path. 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:
77
hooks/gsd-read-guard.js
Normal file
77
hooks/gsd-read-guard.js
Normal file
@@ -0,0 +1,77 @@
|
||||
#!/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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user