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
This commit is contained in:
Kjell Tore Guttormsen 2026-08-14 17:10:04 +02:00
commit c1dabf109d
5 changed files with 154 additions and 10 deletions

View file

@ -208,6 +208,87 @@ process.stdout.write(JSON.stringify({
run_hook "$P"
[ "$HOOK_EXIT" -eq 0 ]; check "Edit: nonexistent file fails open" $?
# --- 8. Ratchet: an already-oversized file must stay editable --------------
# The guard's job is "never let it grow past the limit", not "never let it be
# touched again once over the limit". A file already over 60 lines is the
# NORMAL case a trim session starts from (measured on the real tree,
# 2026-08-14: 23 of the machine's STATE.md files were over 60 lines, one at
# 1405). Denying every write that doesn't land at <=60 in a single shot would
# make every one of those files un-editable except by a perfect one-shot
# rewrite - exactly backwards for a hook meant to make trimming possible.
FIXTURE3="$TMPDIR/c"
mkdir -p "$FIXTURE3"
node -e '
const fs = require("fs");
fs.writeFileSync(process.argv[1], "x\n".repeat(156));
' "$FIXTURE3/STATE.md"
# Write: 156 -> 100 lines. Still over 60, but strictly smaller: allow.
P="$(payload "
process.stdout.write(JSON.stringify({
tool_name: 'Write',
tool_input: { file_path: '$FIXTURE3/STATE.md', content: 'x\\n'.repeat(100) }
}));
")"
run_hook "$P"
[ "$HOOK_EXIT" -eq 0 ]; check "Write: shrinking an oversized file allows, even if still over the limit" $?
# Write: 156 -> 156 lines (untouched size, e.g. only prose changed): allow.
P="$(payload "
process.stdout.write(JSON.stringify({
tool_name: 'Write',
tool_input: { file_path: '$FIXTURE3/STATE.md', content: 'x\\n'.repeat(156) }
}));
")"
run_hook "$P"
[ "$HOOK_EXIT" -eq 0 ]; check "Write: same-size rewrite of an oversized file allows" $?
# Write: 156 -> 200 lines. Still growing an already-oversized file: deny.
P="$(payload "
process.stdout.write(JSON.stringify({
tool_name: 'Write',
tool_input: { file_path: '$FIXTURE3/STATE.md', content: 'x\\n'.repeat(200) }
}));
")"
run_hook "$P"
[ "$HOOK_EXIT" -eq 2 ]; check "Write: growing an already-oversized file still denies" $?
# Write: brand-new STATE.md (no current file) at 61 lines: deny (the ratchet
# must not read "no current file" as "anything goes" -- current defaults to 0).
P="$(payload '
const content = "x\n".repeat(61);
process.stdout.write(JSON.stringify({
tool_name: "Write",
tool_input: { file_path: "/tmp/brand-new-dir-xyz/STATE.md", content }
}));
')"
run_hook "$P"
[ "$HOOK_EXIT" -eq 2 ]; check "Write: creating a new oversized STATE.md still denies" $?
# Edit: same ratchet, via the Edit path. Fixture at 156 lines; old_string is
# the first 60 "x\n" occurrences (a contiguous substring), new_string is 4 of
# them -> projects to 100 lines: still over 60, but smaller than 156. A
# pre-ratchet hook denies this (100 > 60); the ratchet must allow it.
FIXTURE4="$TMPDIR/d"
mkdir -p "$FIXTURE4"
node -e '
const fs = require("fs");
fs.writeFileSync(process.argv[1], "x\n".repeat(156));
' "$FIXTURE4/STATE.md"
P="$(payload "
process.stdout.write(JSON.stringify({
tool_name: 'Edit',
tool_input: {
file_path: '$FIXTURE4/STATE.md',
old_string: 'x\\n'.repeat(60),
new_string: 'x\\n'.repeat(4)
}
}));
")"
run_hook "$P"
[ "$HOOK_EXIT" -eq 0 ]; check "Edit: shrinking an oversized file allows, even if still over the limit" $?
echo ""
echo "state-line-guard-selftest: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ] || exit 1