Tier 3, all five the same defect class (Verifiseringsloven ansikt 4): a
broken or uninstrumented query returning a positive-looking null, consumed
as a fact about the world.
F5 coord-count.sh: a mailbox root that does not exist was byte-identical
to one where nobody has pending mail - zero lines, exit 0, silent
stderr. Now exit 3 + a named stderr line; an existing-but-empty root
stays a silent, clean 0. 3 rather than 2 because 2 already means "you
called me wrong" and this means "the world you named is not there".
F14 coord-count.sh: the header promised exit 0 unconditionally while
--exclude with no value already exited 2. Contract restated as
0/2/3 and pinned as a check on the help TEXT.
F6 board.sh: `git status | wc -l` yields 0 lines whether the tree is
clean or git refused to answer, so a failure printed DRT=0. Now "?",
and BOTH awk consumers handle it - --plan's free-capacity test
compares the field as a string against "0" (a "?" coerces to 0 in
arithmetic and would certify an unmeasured tree as free), and the SUM
roll-up names what it could not add.
F10 board.sh: a scan root that does not exist was skipped in silence and
the empty scan exited 0. Bad roots are now named on stderr; exit 3
only when NO root was scanned. A mix still exits 0 and prints the
board. Replaces an assertion that encoded this defect as a pass.
F13 pre-state-line-guard.mjs: MAX_LINES is overridable via
CLAUDE_STATE_MAX_LINES so the boundary is testable without hardcoding
120 twice. An unusable value denies by name rather than falling back
to the default - a limit that silently did not take effect is the
same defect one layer up.
Every design choice mutation-tested; every negative check carries a
known-positive control. Section 11's first cut was vacuously green (wrong
basename + unexported fixture path) - recorded in CLAUDE.md rather than
quietly fixed, and the section now asserts its own ground truth.
Denominator measured, not estimated: coord-inbox.sh:57 and
coord-order-inbox.sh:60/64 carry the same `|| exit 0` shape and are
deliberately left alone (injection path, prose output, must never fail a
SessionStart) - stated in CLAUDE.md as a bounded gap.
Suites: coord 230->242, board 281->300, guard 40->54, route 69, orders
110, npm 11/11. Verified under system bash 3.2, not just Homebrew 5.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ORDRE 42 (operator, 2026-08-16). Two sessions had their push refused by the
UFW rate limit on port 22, reported that honestly in the coord inbox, and
wrote status=done anyway: board line green, one commit unpushed, published
surface 404. `done` meant "the session finished" where every reader takes it
to mean "the work landed" -- and since `done` drops a repo from the board
plan, `morning --say <repo>` could not reach either of them.
The deny sits on the WRITE, not on session end. Measured against the official
hooks docs rather than assumed: Stop fires "once per turn" with no signal
marking the last one, and its exit 2 "prevents Claude from stopping", so a
repo that genuinely cannot push would get a session that will not end;
SessionEnd is the once-per-session event and cannot block at all.
Fails open on every git uncertainty (no upstream, detached HEAD, missing
remote-tracking ref, not a repo) -- 8 of 44 repos on the real tree have no
upstream, one already status=done. Compares against the branch's own
upstream, never a hardcoded origin/main (three repos sit on master). Selects
the board line with board.sh's own anchor, so prose saying status=done never
triggers it. status=blocked and status=in-progress stay writable in the same
single edit, so the deny can never wedge a session.
state-line-guard-selftest.sh section 10, 17 checks (23 -> 40), including the
mandatory known-positive: status=done with everything pushed still allows.
Both outcomes also verified against real repos -- app-creator (1 unpushed)
denied, repo-mailbox (clean) allowed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P4LMWBQGmufmBU6UdvJZ2E
current.replace(oldStr, newStr) with newStr as a STRING lets JS treat
$-sequences inside it ($&, $`, $', $$, $n) as special replacement
patterns, even though oldStr (the search side) is a plain string. A
new_string documenting old backtick-substitution style ($`cmd`) - the
kind of prose a STATE.md shell-conventions section writes routinely -
triggers it. Measured against the real bug (.claude/STATE.md,
2026-08-15): a 5-line addition on a 112-line file projected to 219
lines and was wrongly denied.
Fix: current.replace(oldStr, () => newStr) - a function replacement is
never pattern-substituted, covering every $-sequence at once. The
replace_all branch (split/join) was never affected.
Direction was always fail-closed (over-blocks, never under-blocks a
real oversize), but it made exactly the STATE.md files that document
shell conventions hard to edit via Edit.
state-line-guard-selftest.sh: 23/23 (+2, section 9: $` as the repro,
$& as a second sequence proving the fix is general).
Also updates CLAUDE.md's pinned selftest counts (197/178/69/21 were
already stale before this session's own additions; now 206/183/69/23).
Operator decision 2026-08-14: the STATE.md convention's line limit moved
from ~60 to ~120 (global CLAUDE.md already updated). Re-bases every
selftest fixture and boundary value that encoded 60 as a literal,
including section 8's ratchet fixtures, so they still exercise the
ratchet rather than degenerating into a flat gate at the new threshold.
Re-verified the real-tree justification at 120: 13 files over the limit,
one at 1496 (was 23 over 60, one at 1405 — left as historical record).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194eV8b6BXNv6aKLovP8TP6
Advisor review caught this before the v0.23.0 tag landed: the guard
compared the projected line count only against the fixed 60-line max,
never against the file's current size, so trimming an already-oversized
STATE.md (e.g. 156 -> 100 lines, still over 60 but smaller) was denied
exactly like growing it would be.
Verified against the real tree: 23 of the machine's STATE.md files are
already over 60 lines today, one at 1405. Shipped as a flat gate, this
hook would have made most of them un-editable except by a single write
landing at <=60 in one shot -- backwards for a guard meant to make
trimming possible.
Fixed with a ratchet: deny only when the projection is over the max AND
larger than the file's current line count (0 for a file that doesn't
exist yet), for both Write and Edit. A compliant file still cannot grow
past the limit and a new file still cannot be created oversized, but an
oversized file can now be edited toward compliance one write at a time.
state-line-guard-selftest.sh: 16 -> 21 checks (new section 8: shrink
allows, same-size allows, grow-while-oversized still denies, new-oversized
still denies). Suite total: 191 + 152 + 69 + 21 = 433.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186kZGKddxfA9N84HqMLbb2
org-ops dispatched a work order (20260814T144553Z) from an /insights sweep
of 160 sessions: a real STATE.md drifted to 155-156 lines before anyone
noticed, and one trim pass on it increased the line count instead of
shrinking it. Prose alone doesn't enforce.
org-ops proposed a PostToolUse hook. Checked against the official hooks
docs first: PostToolUse fires after the tool has already written the file
and cannot block it (confirmed "Can block? No"), only nag afterward. Built
it as PreToolUse instead, the only event that can deny the call before the
file lands.
pre-state-line-guard.mjs denies (stderr + exit 2, matching llm-security's
pre-write-pathguard.mjs) a Write or Edit on any STATE.md whose projected
result exceeds 60 lines. Write projects from the call's own content; Edit
projects from the current on-disk file with old_string replaced by
new_string, honoring replace_all (every occurrence) vs the default (first
occurrence only) the same way the real Edit tool does. Anything the hook
can't project confidently (missing file, old_string not found) is left to
the real tool.
state-line-guard-selftest.sh: 16 checks, including a replace_all fixture
that a first-occurrence-only projection would wrongly allow. Wired into
hooks/hooks.json as PreToolUse on Write|Edit. Version 0.22.0 -> 0.23.0.
Suite total: 191 + 152 + 69 + 16 = 428.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186kZGKddxfA9N84HqMLbb2