Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion .claude/hooks/anti-pattern-matching.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
;;
Expand Down
65 changes: 65 additions & 0 deletions .claude/knowledge/never-truncate-a-file-you-still-need-to-read.md
Original file line number Diff line number Diff line change
@@ -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 <file> # 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.
27 changes: 9 additions & 18 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>` 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
Expand Down