Filed unassigned by the #12795 measurement dev, session session_01CPrUz21stTFhJRUirdc4yw. Found while validating exit-code capture for that card's gate-cost harness; ⛔ out of scope there, not folded in.
What AGENTS.md currently says
The "read the exit code before any pipe" discipline names three safe spellings: redirect-then-capture, set -o pipefail, and ${PIPESTATUS[0]}. Two of those three have a hole.
Measured, one gate, five pipeline shapes, same tree, same commit
pnpm check:i18n on a tree whose CLI is not built (it refuses with PREREQUISITE NOT MET, exit 1):
A: direct RC=1 <- ground truth
B: 2>&1 | cat PIPESTATUS[0]=1 faithful
C: 2>&1 | head -20 PIPESTATUS[0]=0 WRONG — reads green
D: 2>&1 | head -5 PIPESTATUS[0]=0 WRONG — reads green
E: 2>&1 | tail -4 PIPESTATUS[0]=1 faithful
Control, same session, same harness: pnpm check:nul-bytes returns 0 direct — so the instrument returns both values and C/D are readings, not a stuck zero.
Why it happens, and why pipefail does not save you
head -N exits after N lines and closes the read end. The producer then takes EPIPE/SIGPIPE and terminates on a path that yields status 0. ${PIPESTATUS[0]} reports that faithfully — the array is not lying. The pipeline changed the producer's exit code, so every reader of that status, pipefail included, sees a green that the unpiped command never produced. Only redirect-then-capture avoids it, because nothing closes a file early:
cmd > /tmp/out 2>&1; EXIT=$?; tail -40 /tmp/out
Why this matters more than the $? version
$?-after-tail is already recorded and closed. That one is easy to spot: the status obviously belongs to the wrong process. This one is worse in three ways:
- the spelling is on the recommended list, so an agent that followed the rule still gets a false green;
| head -N is the most natural way to look at the start of a gate's output, which is exactly where a refusal banner prints;
- it is silent and it is not falsifiable by re-running — the same pipeline returns the same 0 every time.
Second, smaller half: one gate's own advisory names the wrong shape
scripts/check-i18n-bundles.mjs prints, in its own refusal banner:
piping this gate reports the PIPE's status, so pnpm check:i18n | tail -4 reads green either way
Measured above: | tail -4 reads 1, correctly. | head is the shape that reads green. The advisory is right that piping is dangerous and wrong about which pipe does it, so an agent who trusts it avoids the safe shape and keeps the unsafe one.
Re-check
pnpm check:i18n > /tmp/o 2>&1; echo "direct=$?"
pnpm check:i18n 2>&1 | head -20 > /dev/null; echo "head=${PIPESTATUS[0]}"
pnpm check:i18n 2>&1 | tail -4 > /dev/null; echo "tail=${PIPESTATUS[0]}"
⛔ Reverse-check any zero against a gate known green in the same run, never against a substring of the term under test.
Severity not judged; observation-class. ⛔ Not graded, not routed.
Filed unassigned by the #12795 measurement dev, session
session_01CPrUz21stTFhJRUirdc4yw. Found while validating exit-code capture for that card's gate-cost harness; ⛔ out of scope there, not folded in.What AGENTS.md currently says
The "read the exit code before any pipe" discipline names three safe spellings: redirect-then-capture,
set -o pipefail, and${PIPESTATUS[0]}. Two of those three have a hole.Measured, one gate, five pipeline shapes, same tree, same commit
pnpm check:i18non a tree whose CLI is not built (it refuses with PREREQUISITE NOT MET, exit 1):Control, same session, same harness:
pnpm check:nul-bytesreturns 0 direct — so the instrument returns both values and C/D are readings, not a stuck zero.Why it happens, and why
pipefaildoes not save youhead -Nexits after N lines and closes the read end. The producer then takes EPIPE/SIGPIPE and terminates on a path that yields status 0.${PIPESTATUS[0]}reports that faithfully — the array is not lying. The pipeline changed the producer's exit code, so every reader of that status,pipefailincluded, sees a green that the unpiped command never produced. Only redirect-then-capture avoids it, because nothing closes a file early:Why this matters more than the
$?version$?-after-tailis already recorded and closed. That one is easy to spot: the status obviously belongs to the wrong process. This one is worse in three ways:| head -Nis the most natural way to look at the start of a gate's output, which is exactly where a refusal banner prints;Second, smaller half: one gate's own advisory names the wrong shape
scripts/check-i18n-bundles.mjsprints, in its own refusal banner:Measured above:
| tail -4reads 1, correctly.| headis the shape that reads green. The advisory is right that piping is dangerous and wrong about which pipe does it, so an agent who trusts it avoids the safe shape and keeps the unsafe one.Re-check
⛔ Reverse-check any zero against a gate known green in the same run, never against a substring of the term under test.
Severity not judged; observation-class. ⛔ Not graded, not routed.