From 13bc324583db0dab38864464947a7981355faa2a Mon Sep 17 00:00:00 2001 From: Terada Kousuke Date: Sun, 5 Apr 2026 21:30:14 +0900 Subject: [PATCH 1/2] feat(hook): add fact-checking PreToolUse hook for Write/Edit tools (#53) Advisory hook that warns when Write/Edit tools are invoked without evidence of prior file reading. The hook checks tool input for research indicators (grep results, line references, etc.) and emits a stderr warning that gets injected into LLM context. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../hooks/enforce-factcheck-before-edit.sh | 22 +++++ .opencode/opencode.jsonc | 12 +++ packages/opencode/test/hook/factcheck.test.ts | 94 +++++++++++++++++++ 3 files changed, 128 insertions(+) create mode 100755 .opencode/hooks/enforce-factcheck-before-edit.sh create mode 100644 packages/opencode/test/hook/factcheck.test.ts diff --git a/.opencode/hooks/enforce-factcheck-before-edit.sh b/.opencode/hooks/enforce-factcheck-before-edit.sh new file mode 100755 index 000000000000..b5377e6eebde --- /dev/null +++ b/.opencode/hooks/enforce-factcheck-before-edit.sh @@ -0,0 +1,22 @@ +#!/bin/sh +# enforce-factcheck-before-edit.sh +# PreToolUse hook: warns when Write/Edit tools are invoked without evidence of prior research +# Exit 0 = pass (advisory only), stderr message becomes hook context + +TOOL="$OPENCODE_TOOL_NAME" +INPUT="$OPENCODE_TOOL_INPUT" + +# Only apply to write/edit tools +case "$TOOL" in + write|Write|edit|Edit) ;; + *) exit 0 ;; +esac + +# Check if tool input contains evidence of prior file reading +if echo "$INPUT" | grep -qiE '(based on reading|as shown in|from the file|grep result|read tool output|line [0-9]+)'; then + echo "FACT-CHECK: Evidence of prior research detected" >&2 + exit 0 +fi + +echo "FACT-CHECK WARNING: File edit attempted without evidence of reading the target file first. Best practice: read the file before editing." >&2 +exit 0 diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index 170987412a15..d77c61af10b7 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -122,4 +122,16 @@ "github-triage": false, "github-pr-search": false, }, + "hooks": { + "PreToolUse": [ + { + "command": ".opencode/hooks/enforce-factcheck-before-edit.sh", + "matcher": "write", + }, + { + "command": ".opencode/hooks/enforce-factcheck-before-edit.sh", + "matcher": "edit", + }, + ], + }, } diff --git a/packages/opencode/test/hook/factcheck.test.ts b/packages/opencode/test/hook/factcheck.test.ts new file mode 100644 index 000000000000..d0f8ea7836fd --- /dev/null +++ b/packages/opencode/test/hook/factcheck.test.ts @@ -0,0 +1,94 @@ +import path from "node:path" +import { describe, expect, test } from "bun:test" +import { runHook, type HookEnv } from "../../src/hook" + +const SCRIPT_PATH = path.resolve( + import.meta.dir, + "../../../../.opencode/hooks/enforce-factcheck-before-edit.sh", +) + +function makeEnv(toolName: string, toolInput: string): HookEnv { + return { + OPENCODE_HOOK_EVENT: "PreToolUse", + OPENCODE_TOOL_NAME: toolName, + OPENCODE_TOOL_INPUT: toolInput, + OPENCODE_PROJECT_DIR: "/tmp/test", + OPENCODE_SESSION_ID: "test-session", + } +} + +describe("enforce-factcheck-before-edit", () => { + test("passes non-edit tools without message", async () => { + const result = await runHook({ command: SCRIPT_PATH }, makeEnv("bash", "{}")) + expect(result.action).toBe("pass") + expect(result.message).toBeUndefined() + }) + + test("passes non-edit tool 'read'", async () => { + const result = await runHook({ command: SCRIPT_PATH }, makeEnv("read", '{"file":"/tmp/x"}')) + expect(result.action).toBe("pass") + expect(result.message).toBeUndefined() + }) + + test("warns on Write without evidence", async () => { + const result = await runHook( + { command: SCRIPT_PATH }, + makeEnv("Write", '{"file_path":"/tmp/test.ts","content":"hello"}'), + ) + expect(result.action).toBe("pass") + expect(result.message).toContain("FACT-CHECK WARNING") + }) + + test("warns on edit without evidence", async () => { + const result = await runHook( + { command: SCRIPT_PATH }, + makeEnv("edit", '{"old_string":"foo","new_string":"bar"}'), + ) + expect(result.action).toBe("pass") + expect(result.message).toContain("FACT-CHECK WARNING") + }) + + test("passes Edit with evidence phrase 'based on reading'", async () => { + const result = await runHook( + { command: SCRIPT_PATH }, + makeEnv("Edit", '{"old_string":"based on reading the config"}'), + ) + expect(result.action).toBe("pass") + expect(result.message).toContain("Evidence of prior research detected") + }) + + test("passes Write with evidence phrase 'line 42'", async () => { + const result = await runHook( + { command: SCRIPT_PATH }, + makeEnv("Write", '{"content":"fix at line 42"}'), + ) + expect(result.action).toBe("pass") + expect(result.message).toContain("Evidence of prior research detected") + }) + + test("passes write with evidence phrase 'grep result'", async () => { + const result = await runHook( + { command: SCRIPT_PATH }, + makeEnv("write", '{"content":"grep result shows usage"}'), + ) + expect(result.action).toBe("pass") + expect(result.message).toContain("Evidence of prior research detected") + }) + + test("passes Edit with evidence phrase 'read tool output'", async () => { + const result = await runHook( + { command: SCRIPT_PATH }, + makeEnv("Edit", '{"old_string":"per read tool output"}'), + ) + expect(result.action).toBe("pass") + expect(result.message).toContain("Evidence of prior research detected") + }) + + test("never blocks (advisory only)", async () => { + const result = await runHook( + { command: SCRIPT_PATH }, + makeEnv("Write", '{"file_path":"/tmp/x","content":"no evidence here"}'), + ) + expect(result.action).toBe("pass") + }) +}) From 3464d575156c4368cfc68fd49a1cfb2396846feb Mon Sep 17 00:00:00 2001 From: Terada Kousuke Date: Sun, 5 Apr 2026 21:46:32 +0900 Subject: [PATCH 2/2] fix(hook): use printf for POSIX portability and remove dead case branches (#83) Co-Authored-By: Claude Opus 4.6 (1M context) --- .opencode/hooks/enforce-factcheck-before-edit.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.opencode/hooks/enforce-factcheck-before-edit.sh b/.opencode/hooks/enforce-factcheck-before-edit.sh index b5377e6eebde..ab286e4907a6 100755 --- a/.opencode/hooks/enforce-factcheck-before-edit.sh +++ b/.opencode/hooks/enforce-factcheck-before-edit.sh @@ -8,12 +8,12 @@ INPUT="$OPENCODE_TOOL_INPUT" # Only apply to write/edit tools case "$TOOL" in - write|Write|edit|Edit) ;; + write|edit) ;; *) exit 0 ;; esac # Check if tool input contains evidence of prior file reading -if echo "$INPUT" | grep -qiE '(based on reading|as shown in|from the file|grep result|read tool output|line [0-9]+)'; then +if printf '%s\n' "$INPUT" | grep -qiE '(based on reading|as shown in|from the file|grep result|read tool output|line [0-9]+)'; then echo "FACT-CHECK: Evidence of prior research detected" >&2 exit 0 fi