Files
msd-core/hooks/msd-read-guard.js
Jakub Zych 6cfa0c55d2 refactor: drop 12 runtimes, keep Claude, Codex, OpenCode, Cursor, ZCode, Antigravity
Removes kilo, kimi, kimi-code, copilot, windsurf, augment, trae, qwen, hermes,
cline, codebuddy and pi end to end: capability descriptors, installer branches
and converters (bin/install.js 14.9k -> 11.2k lines), TypeScript converters,
hook surfaces and runtime homes, review lanes qwen/kimi-code, the two pi
migrations, Kimi payload normalization in the hook guards, dead hostBehaviors
vocabulary, launcher home probes, fixtures, runtime-specific tests and the
prose that presented them as supported.

Installer output for the six kept runtimes is byte-identical to before the
prune. The Kimi tool-vocabulary tests in workflow-guard, read-guard and
read-injection-scanner are left in place pending a decision.
2026-10-06 20:02:40 +02:00

117 lines
4.6 KiB
JavaScript

#!/usr/bin/env node
// msd-hook-version: {{MSD_VERSION}}
// MSD 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');
const { HOOK_ON_CRASH, allow, crash } = require('./lib/hook-exit.js');
// This guard is pure advisory UX — a reminder to Read before Write/Edit on
// non-Claude-Code runtimes. A crash here must not block a legitimate Write/
// Edit; the worst outcome of failing open is the model hitting the runtime's
// own read-before-edit rejection it was trying to help avoid (#3911).
const ON_CRASH = HOOK_ON_CRASH.ALLOW;
let input = '';
const stdinTimeout = setTimeout(() => allow(undefined), 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') {
allow(undefined);
}
// 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) {
allow(undefined);
}
// #2595 (review Major 3, sibling sweep): typed read — same class as the
// worktree guard's, advisory-only here (no exit(2) path in this hook).
const filePath = typeof data.tool_input?.file_path === 'string'
? data.tool_input.file_path
: '';
if (!filePath) {
allow(undefined);
}
// 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) {
allow(undefined);
}
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.',
code: 'READ_BEFORE_EDIT',
fileName,
},
};
process.stdout.write(JSON.stringify(output));
} catch {
// Silent fail — never block tool execution.
// ON_CRASH is declared ALLOW at module top: this preserves today's
// exit(0) fail-open behavior exactly (#3911).
crash(ON_CRASH, undefined);
}
});