The full-suite hook judges heredoc prose as a command
Rule 3 of .claude/hooks/no-full-suite.sh requires two matches in $scan:
- a
test/-shaped glob ((test|tests)/[A-Za-z0-9_]*\*[A-Za-z0-9_]*\.(npy|pas|c|py|zig|rs)), and - a shell-loop word (
for|while|xargs|parallel, orfind -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.