Commit graph

4 commits

Author SHA1 Message Date
b5c860eb03 fix(state-line-guard): Edit path used String.replace, not a function
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).
2026-08-15 20:37:18 +02:00
44fb34ab72 fix(hooks): raise state-line-guard MAX_LINES from 60 to 120
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
2026-08-14 21:35:31 +02:00
c1dabf109d fix(hooks): state-line-guard ratchets against current size, not a flat gate
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
2026-08-14 17:10:04 +02:00
f39c0df929 feat(hooks): enforce STATE.md's ~60-line convention with a PreToolUse guard
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
2026-08-14 17:01:20 +02:00