Skip to content

[finding] ${PIPESTATUS[0]} is documented as a SAFE form but reads 0 for a red gate when the downstream reader closes the pipe early (| head) #12979

Description

@os-elon

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.

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions