diff --git a/.claude/hooks/anti-pattern-matching.sh b/.claude/hooks/anti-pattern-matching.sh index 9cad0d7e4..a616fd72c 100755 --- a/.claude/hooks/anti-pattern-matching.sh +++ b/.claude/hooks/anti-pattern-matching.sh @@ -21,20 +21,39 @@ tool="$(printf '%s' "$input" | jq -r '.tool_name // ""')" RULE='ANTI-MUSTER-REGEL (Operator-Direktive): Grep/grep/rg/sed/tail/head sind NUR schnelle Discovery-Suche ueber den kompletten Corpus (ein Symbol/eine Datei lokalisieren) — NIEMALS Ersatz fuers Verstehen. Auf einen Treffer NICHT handeln (editieren, loeschen, beurteilen, "verstanden" behaupten), bevor die betroffene Datei VOLLSTAENDIG mit dem Read-Tool gelesen wurde. Verstehen = ganzes Read, kein Snippet. (Grund: geloeschter Code, der nur gemustert, nie gelesen wurde.)' +# Destructive-prepend guard (operator directive, 2026-08-30, after +# open(p, "w").write(entry + open(p).read()) truncated PR_ARC_INVENTORY.md +# 5876 -> 32 lines in #1081; restored #1082; law: +# .claude/knowledge/never-truncate-a-file-you-still-need-to-read.md). +# Non-blocking: injects the rule when a Bash command combines opening a file +# for writing with reading in the same expression/pipeline. +PREPEND_RULE='DESTRUCTIVE-PREPEND PROHIBITED (P0, locked 2026-08-30): never open a file for writing/truncation in the same expression or pipeline that still READS that file. open(p, "w") truncates BEFORE the argument open(p).read() runs (destroyed 5876 lines of PR_ARC_INVENTORY.md in #1081); "sort f > f" is the same defect. Prepend = read into a variable, compose in memory, THEN write — or use the Edit tool. Mandatory post-check after every ledger write: wc -l; an append-only file that got SHORTER is always a defect. Law: .claude/knowledge/never-truncate-a-file-you-still-need-to-read.md' + emit() { jq -n --arg c "$RULE" \ '{hookSpecificOutput: {hookEventName: "PreToolUse", additionalContext: $c}}' } +emit_prepend() { + jq -n --arg c "$PREPEND_RULE" \ + '{hookSpecificOutput: {hookEventName: "PreToolUse", additionalContext: $c}}' +} + case "$tool" in Grep) emit ;; Bash) cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // ""')" + # Destructive-prepend shape: an open-for-write and a .read() of a file in + # the same command (Python one-liner or heredoc). Heuristic, non-blocking + # — false positives only cost an injected reminder. + if printf '%s' "$cmd" | grep -Eq 'open\([^)]*,[[:space:]]*\\*['"'"'"]w' \ + && printf '%s' "$cmd" | grep -q '\.read()'; then + emit_prepend # Match grep/rg/sed/tail/head as a command word (start, or after a # pipe/semicolon/&&/whitespace), not as a substring of another word. - if printf '%s' "$cmd" | grep -Eq '(^|[|&;]|[[:space:]])(grep|rg|sed|tail|head)([[:space:]]|$)'; then + elif printf '%s' "$cmd" | grep -Eq '(^|[|&;]|[[:space:]])(grep|rg|sed|tail|head)([[:space:]]|$)'; then emit fi ;; diff --git a/.claude/knowledge/never-truncate-a-file-you-still-need-to-read.md b/.claude/knowledge/never-truncate-a-file-you-still-need-to-read.md new file mode 100644 index 000000000..9ed9e1488 --- /dev/null +++ b/.claude/knowledge/never-truncate-a-file-you-still-need-to-read.md @@ -0,0 +1,65 @@ +# Never open a file for writing in the same expression that still reads it + +> READ BY: every session and worker that writes any board, ledger, or +> append-only file (`.claude/board/*`, `EPIPHANIES.md`, `PR_ARC_INVENTORY.md`, +> plans, census docs) — and any executor running a scripted prepend. + +## Status: CONTRACT (locked by operator directive, 2026-08-30) + +Operator, verbatim: **"how about making that faulty one liner prohibited"** — +after the pattern destroyed 5876 lines of `PR_ARC_INVENTORY.md`. + +## The One-Line Rule + +A file opened for writing/truncation may not be READ anywhere in the same +expression, statement, or shell pipeline — prepend is read-into-variable → +compose-in-memory → write, or the `Edit` tool (read-anchored by construction). + +## The incident (evidence class: observed) + +The #1081 board-hygiene commit ran: + +```python +open(p, "w").write(entry + "\n" + open(p).read()) # PROHIBITED +``` + +Python evaluates `open(p, "w")` — truncating the file to zero bytes — before +the argument's `open(p).read()` runs. The read-back returned `""`; the +append-only ledger collapsed **5876 → 32 lines** on main, leaving a dangling +#1079 self-reference as the visible wound. Restored same day in #1082 +(merge `82679c3a`), byte-identical from the parent blob `cdf8c15a^1`. + +## Consequences (all load-bearing) + +1. Prohibited in every language and shape: `sort f > f` (shell truncates `f` + before `sort` reads it), any Python expression combining `open(p, "w")` + with a read of `p`, any equivalent. Do not rely on remembering which + language evaluates arguments first — the rule is shape-based, not + language-based. +2. Safe prepend is three steps: `body = read(p)` → compose → `write(p)`. + For board files the default is the `Edit` tool anyway. +3. This is the sharper edge of the existing "Read before Write, always" P0: + read-before-write applies WITHIN a single expression's evaluation order, + not just across tool calls. + +## Falsifier (machine-runnable, mandatory after every ledger write) + +```bash +wc -l # compare against the pre-write count +``` + +**An append-only file that got SHORTER is always a defect** — no exceptions. +Supports: catching any truncation immediately. Does **not** prove: content +correctness or ordering — a same-length corruption passes this check. + +A non-blocking PreToolUse guard (`.claude/hooks/anti-pattern-matching.sh`) +injects this rule when a Bash command matches the write-while-reading shape. + +## Cross-reference / retrieval footer + +- Restore PR: lance-graph **#1082** (merge `82679c3a`); prohibition PR **#1083** + (merge `352005d3`); this file's Goldstandard-format landing: its own PR. +- Board entry: `EPIPHANIES.md` `E-DESTRUCTIVE-PREPEND-TRUNCATES-BEFORE-READ-1` + (2026-08-30). +- Census trap 10: `docs/architecture/COGNITIVE-FABRIC-CENSUS-2026-08-30.md` §8.3. +- CLAUDE.md § In-Session Orchestration Discipline carries the short pointer. diff --git a/CLAUDE.md b/CLAUDE.md index f1e8a4127..81b67148d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1395,24 +1395,15 @@ instead of *built from state*. If you see this on your own commit, you just overwrote committed work. Revert with `git restore ` and use `Edit` for any genuine refinement. -**P0 Rule: the destructive-prepend one-liner is PROHIBITED** -(added 2026-08-30 after it destroyed 5876 lines of `PR_ARC_INVENTORY.md` -in #1081; restored in #1082; full entry -`E-DESTRUCTIVE-PREPEND-TRUNCATES-BEFORE-READ-1`, census trap 10): - -```python -open(p, "w").write(entry + open(p).read()) # NEVER — truncates before it reads -``` - -`open(p, "w")` truncates the file BEFORE the argument's `open(p).read()` -runs; the read-back is empty and the file's history is gone. Prohibited in -every form: any expression, statement, or shell pipeline that opens a file -for writing/truncation while it still needs to READ that same file -(`sort f > f` is the same defect). Prepend in three steps — read into a -variable, compose in memory, write — or use the `Edit` tool, which is -read-anchored by construction. **Mandatory post-check after every board / -ledger write: `wc -l` the file. An append-only file that got SHORTER is -always a defect.** +**P0 Rule: the destructive-prepend one-liner is PROHIBITED** — never open a +file for writing in the same expression/pipeline that still reads it +(`open(p, "w").write(x + open(p).read())`, `sort f > f`). Mandatory +post-check after every ledger write: `wc -l` — an append-only file that got +SHORTER is always a defect. Full law, incident receipts (#1081/#1082), +falsifier and consequences: +`.claude/knowledge/never-truncate-a-file-you-still-need-to-read.md` +(locked 2026-08-30; board entry +`E-DESTRUCTIVE-PREPEND-TRUNCATES-BEFORE-READ-1`, census trap 10). **Tool-reach reminder for deferred tools.** `AskUserQuestion`, `TodoWrite`, `WebSearch`, `WebFetch` are namechecked in the Claude Code system prompt but