import * as fs from "node:fs"; import * as path from "node:path"; import { applyEdits, readFileSafe } from "../utils.ts"; import { runGates, type GateEdit } from "../gates/run-gates.ts"; import { loadIndexForHook, notifyNoContracts } from "./index-loader.ts"; import "../gates/checkers/index.ts"; type HookEvent = { hook_event_name?: string; tool_name?: string; tool_input?: { file_path?: string; old_string?: string; new_string?: string; edits?: { old_string?: string; new_string?: string }[]; content?: string; }; cwd?: string; }; const notifyCtx = (): { cwd: string; ui: { notify: (m: string) => void } } => ({ cwd: process.cwd(), ui: { notify: (m) => process.stderr.write(`[Module Gate] ${m}\n`), }, }); async function main(): Promise { let raw: string; try { raw = fs.readFileSync(0, "utf-8"); } catch { process.exit(0); } let event: HookEvent; try { event = JSON.parse(raw); } catch { process.stderr.write("[Module Gate] hook: invalid JSON input; allowing tool call.\n"); process.exit(0); } if (event.hook_event_name !== "PreToolUse") process.exit(0); if ( event.tool_name !== "Edit" && event.tool_name !== "MultiEdit" && event.tool_name !== "Write" ) { process.exit(0); } const cwd: string = event.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd(); const toolInput = event.tool_input ?? {}; const filePath: string | undefined = toolInput.file_path; if (!filePath) process.exit(0); const absPath = path.resolve(cwd, filePath); const before = readFileSafe(absPath); let after: string; if (event.tool_name === "Edit") { after = applyEdits(before, [ { oldText: toolInput.old_string ?? "", newText: toolInput.new_string ?? "" }, ]); } else if (event.tool_name === "MultiEdit") { const edits: GateEdit[] = Array.isArray(toolInput.edits) ? toolInput.edits.map((e) => ({ oldText: e.old_string ?? "", newText: e.new_string ?? "", })) : []; after = applyEdits(before, edits); } else { after = toolInput.content ?? ""; } const { index, config } = await loadIndexForHook(cwd); if (index.contracts.length === 0) { notifyNoContracts(notifyCtx()); process.exit(0); } const result = runGates(filePath, [{ oldText: before, newText: after }], cwd, index, config, before); if (result?.block) { process.stderr.write(`${result.reason}\n`); process.exit(2); } process.exit(0); } main().catch((err) => { const message = err instanceof Error ? err.message : String(err); process.stderr.write(`[Module Gate] hook internal error: ${message}\n`); process.exit(0); });