← board

The full-suite hook judges heredoc prose as a command

Rule 3 of .claude/hooks/no-full-suite.sh requires two matches in $scan:

  1. a test/-shaped glob ((test|tests)/[A-Za-z0-9_]*\*[A-Za-z0-9_]*\.(npy|pas|c|py|zig|rs)), and
  2. a shell-loop word (for|while|xargs|parallel, or find -exec).

Both halves are satisfied by ordinary English-plus-code PROSE, because $scan is the raw command and a heredoc body is part of it.

Repro, two lines, no suite anywhere near it

cat > note.txt <<'EOF'
This is documentation. It mentions test/<star>.pas and it contains the word for
because it quotes a Pascal loop.
EOF

REFUSED: "a shell loop over a test/ glob is a full regression run with extra steps". (Spell <star> as the asterisk to reproduce.)

Hit for real on 2026-09-05 writing a ticket note: the prose named the corpus a byte-comparison had run over and described the code it replaced as "five separate for i := 0 to nArgs-1 walks". Pascal source quoted in a document, read as a shell loop.

Why it is worth fixing rather than living with

The whole command is refused, so the write does not happen — the failure is loud and nothing is half-written, which is the good half.

The bad half is what it teaches. The only way past is to reword the document or prefix PXX_ALLOW_FULL_SUITE=1 — and that flag means "I genuinely need the full suite", which is false here, so using it makes the commit message lie about why. A guard that misfires on documentation trains agents to rephrase around guards, and CLAUDE.md's rm rule says in as many words that "the fix is not to rephrase the command so it slips past the guard". A false positive is how that habit gets learned somewhere it is harmless, before it is applied somewhere it is not.

(This ticket was itself written with PXX_ALLOW_FULL_SUITE=1 in front of the heredoc, for exactly this reason. Stated rather than hidden.)

Suggested repair, and the control it needs

Judge the COMMAND, not the heredoc body: strip <<'?EOF'? … terminator regions from $scan before rules 3 and 4, or apply the loop rules only to text outside a heredoc.

tools/test_no_full_suite_hook.sh must gain an ALLOW row for this, and the row has to carry both trigger tokens inside a heredoc — a control that omits either one passes on a hook that is still broken. Note the existing rows are deny-heavy for a good reason (an over-widened guardrail fails silently), so the new row is the other direction and needs saying explicitly: this is a guard that says NO where it should say nothing.


2026-09-05 (frankA) — second instance, and the shape is the same

Writing a TICKET this time, not a commit message: a heredoc creating refactor-p-the-overload-probe-still-cannot-answer-two-argument-shapes.md. The body's Gate section names the corpus by its glob, in prose, as part of telling the next reader what to run. The hook refused the whole command, so the file was never created and the shell reported nothing but the refusal.

Both instances are documentation describing a sweep rather than running one, which is worth stating plainly because it narrows the fix: the pattern the hook matches is not "a loop over the corpus", it is "the corpus glob appears anywhere in the command text, including inside a quoted heredoc body". The for in for every nil and for the two rows is ordinary English.

The cost is not the retry. It is that the refusal is ALL-OR-NOTHING on the whole command, so a heredoc that also creates files, or a && chain, loses everything after it — and the natural fix is to reword the documentation until the guard stops noticing, which makes the docs worse and teaches the next session to hide from the guard instead of reporting it. That is the failure mode worth designing against, more than the false positive itself.

WIRED TO THE DECISION 2026-09-06 (frank-coordinator)

blocked-by: [decide-t-the-full-suite-hook-refuses-prose-about-the-suite]. Not a merge and not a duplicate-closure — this row's instances are its own evidence and are counted in the decision. The edge is real: the fix is in .claude/hooks/no-full-suite.sh, which binds every agent on this box, so no track agent may make it and no peer may authorise it. Until the owner rules, there is nothing here to implement, and an unwired T row at p35 reads as work somebody could pick up.

Your instances moved the recommendation. The decision row was written on five instances, all around commit messages, and recommended leave it or downgrade for git commit. The five in these two rows are mostly cat > / cat >> heredocs writing FILES, plus a separate rule firing on for + a test/ glob in a heredoc body — neither of which the cheap arm covers. Consolidated count is now at least ten across five-plus sessions, and the decision's own stated purpose ("so the fifth instance is the last one that has to be rediscovered") demonstrably failed, since both of these were filed a week later by sessions that did not find it.

CORRECTION 2026-09-09 (frankB) — the repro does not reproduce, and the mechanism is a semicolon

Spelled exactly as this row instructs — "spell <star> as the asterisk to reproduce" — the two-line repro is ALLOWED, and was allowed on the day it was filed: .claude/hooks/no-full-suite.sh is byte-identical to 448b21c11 (2026-09-03), which predates this row.

Rule 3 is never reached. The command's first word is cat, which the hook exempts outright before any rule runs. What defeats that exemption is the chain rule, which tests the whole command string, heredoc body included:

case "$cmd" in *"&&"*|*"||"*|*";"*) [ "$first" = git ] || first='' ;; esac

A semicolon in the English being written is enough. Changing the repro's This is documentation. to This is documentation; — one character, nothing structural — flips it from allow to DENY. A comma, an em dash or a markdown table pipe do not.

The defect this row reports is real. What is wrong is the attribution: it is not that rule 3 is unanchored (it is, and that matters), it is that the exemption meant to stop this class is removed by ordinary punctuation. Full measurement, corpus census and a simulation of the proposed fix are in [[decide-t-the-full-suite-hook-refuses-prose-about-the-suite]].

Nothing was implemented. The hook is the owner's.