---
name: exit-status-after-a-truncation-pipe-measures-the-truncator
description: "`EXIT=$?` after `| tail`/`| head` reads the TRUNCATOR's status (always 0), not the command's — and the idiom fails hardest exactly where the number is about to become evidence in a ticket."
metadata: 
  node_type: memory
  type: feedback
  originSessionId: 0518699d-acae-4cbc-9b0f-85fc47b8ca8f
  modified: 2026-08-25T21:57:31.723Z
---

`cmd 2>&1 | tail -20; echo "EXIT=$?"` measures **`tail`**, which exits 0 unconditionally. Without
`set -o pipefail`, `$?` after a pipeline is the LAST element's status. The command's own code is
gone.

**This filed a false BUGFIX.** releases#153 ("`UPDATE_FINISH_REFUSED` exits 0") was measured through
exactly that line on 2026-08-04. The product returned 3 the whole time — verified at four trees
(v0.50.0, v0.52.0, v0.53.0, c62904e7), and an e2e already pinned the arm non-zero. A lane was
dispatched, briefed, and re-scoped before the instrument was questioned.

**Why the idiom survives in people who know better** (deployah's own RCA of it): the correct form
gets reached for when the exit status is *obviously* the point — their gradlew test verdicts used
`${PIPESTATUS[0]}` correctly on the same box. The broken one lands on **truncation** pipes (`tail`,
`head`, `head -c`) because those read as *formatting* rather than as pipeline elements, so "what is
the last element of this pipeline" never gets asked. And truncation is what you add when you are
trimming output **in order to quote it** — so the idiom fails hardest precisely where the number is
about to become evidence in a ticket.

**Do:** `cmd > f 2>&1; rc=$?; tail -20 f` — capture first, trim after. Or `${PIPESTATUS[0]}`. Or
`set -o pipefail`. Same trap in my own runs: a `| tail -N` on a `cargo test` line hands me tail's
status, which is why I read the RESULT LINE ("test result: FAILED"), never the pipeline's exit —
and why an unpiped re-run is required before reporting a checker's exit code (see
[[verify-a-checkers-exit-code-unpiped]]).

**The tell, and it is the generalizable half:** a control and a treatment reporting the *identical*
number is evidence about the METER, not about the subject. #153's own filing contained it — a
refused roll and a successful roll both reported `EXIT=0` — and that agreement was read as the
finding instead of as the instrument fault it was. Related: [[dont-take-a-diagnosis-as-measured]].

## PIPESTATUS IS NOT PROTECTION — the pipe can MANUFACTURE a false status (2026-08-22, #159)

`traceable-reqs check 2>&1 | head -3` with `${PIPESTATUS[0]}` read **101**. Not the tool's
verdict and not `head`'s 0 either: `head` CLOSED the pipe, the producer took SIGPIPE and died,
and PIPESTATUS faithfully reported the death the pipe caused. So the truncation pipe does not
merely HIDE the producer's status — it can INVENT one. Reaching for PIPESTATUS "to do it
properly" does not help when the pipe changes the producer's fate. Redirected to a file, the
real answer was exit **1**.

Same lane, one hour later, I did it again: `cargo clippy … | tail -12; echo CLIPPY_EXIT=$?`
read 0. Clippy really WAS clean, so the reading was correct and the instrument was still
wrong — which is the version that never gets caught, because nothing disagrees with it.

**THE TELL IS THE REPORTING, NOT THE PIPE.** Every instance of this I have produced —
three now — happened while trimming output *to quote it to someone*. The question that fires
reliably is **"am I about to quote this?"**, not "is this a pipe?". For anything that becomes
evidence: redirect to a file, echo the status, read the file. Never a truncation pipe, with or
without PIPESTATUS.

## THIRD SHAPE, NO PIPE AT ALL: a wrapper that CAPTURES a status ends with the capture (2026-08-22, W5)

`( cd <lane> && cargo nextest … > leg.raw 2>&1 ); echo $? > leg.exit` — the wrapper's LAST command
is the `echo`, which succeeds, so the wrapper exits **0** over a leg that exited **100**. todlando
read the runner's "exit code 0" summary against a leg file reading 100 and was about to file a
task-runner misreport; the minimal probe `( exit 100 ); echo $? > f; FINAL=$?` retracted it — the
runner told the truth about the WRAPPER. Filed as **IR-60** (about our script shape; nothing to file
against the runner). Same family, and it proves the class is not about pipes: **a truncation pipe, a
flattened `$?`, and a wrapper whose last command is the capture** are three shapes with ONE trigger,
and the trigger is the reporting. Remedy order: read the LEG's exit file, never a wrapper's status;
if a wrapper's status must mean something, re-raise it (`exit "$(cat leg.exit)"`).

Also banked from that exchange: my own `git cherry-pick … | tail -20; echo EXIT=$?` read tail's 0
during the Lane 3 assembly — the CONFLICT TEXT was what measured the pick. Same trigger: I was
trimming the output to quote it.

## FOURTH SHAPE: the background-task COMPLETION NOTIFICATION is a wrapper status, and I cited an exit file I never read (2026-08-25, #222 gate)

My ssh background leg ended `echo \$? > /tmp/…exit; cat …exit` — perfectly correct capture — and I
still reported "Linux clippy exit 0 from its exit file" to the builder AND in my summary WITHOUT
EVER READING THE OUTPUT: I read the harness's task notification, "completed (exit code 0)", which
is ssh's/the wrapper's status. The output file said **127, `cargo: command not found`** (non-login
ssh shell PATH — a trap the builder had named to me IN THE SAME LANE, correction #3 of his own
report). Nothing was linted; the leg's exit file existed, said 127, and my report claimed it said
0. The exit-file discipline is only the WRITE half — **a verdict is quoted only after the exit
file's content has been READ in this transcript**; a completion notification never substitutes,
because it reports the outermost wrapper by construction. Caught one message later only because
the SECOND run finished suspiciously fast and I read that one. The retraction went out before the
re-run's verdict came back (real answer after `~/.cargo/bin/cargo`: 0, with the changed crates'
Checking lines counted).

**Confirmed working in the other direction, 2026-08-25 (#225 wake):** the notification for my
serialized proof run said "completed (exit code 0)"; the exit FILE's content read **100** (one
test failed). Reading the file first was the wake note's own instruction, and it flipped the
r4 verdict from CUT to HOLD — the discipline is not ceremony, it changes release decisions.
Same session, the 127/cargo-not-found face repeated verbatim on the first alone-probe (banked
trap, caught immediately by reading the file).

## FIFTH SHAPE: the EXIT FILE ITSELF was flattened — the producer had no exit semantics (2026-08-29, v0.66.0 golden r3)

The inversion of face four: there I quoted an exit file I never read; here I read the file
correctly **and the file lied**, because I wrote the producer. My watcher was
`gh run watch <id> > raw 2>&1; echo $? > R3_watch.exit`. **`gh run watch` returns 0 regardless of
the run's conclusion unless you pass `--exit-status`.** I omitted it. `R3_watch.exit` read `0`
while the run's API `conclusion` read `failure`. Capture was textbook — the CAPTURED THING had no
verdict in it.

The tell that caught it was the banked one and it fired on my own artifact: my r2 watcher's exit
file ALSO read `0`, on a run I had already established was red — **a control and a treatment
returning the identical number** (see [[identical-readings-across-opposite-outcomes-indict-the-meter]]).
I had reported r2 correctly only because I happened to open a DIFFERENT session's watcher file,
one whose author had passed `--exit-status` and which read `1`. Luck of which path I opened, not
method — and I had meanwhile stated "the verdict comes from the exit file content" to a gater as
a rule, twice.

**The durable correction: "read the exit FILE" is necessary, never sufficient. An exit file is
only as good as its producer's exit semantics.** Before trusting one, ask what the captured
command returns on failure — many watch/poll/report verbs exit 0 on a red outcome by design
(`gh run watch` bare, `gh pr checks` without `--watch --fail-fast`, any `--format`-style reporter).
For a CI run the authority is the run's own `conclusion` field from the API, which is a property
of the RUN and not of any process I wrapped around it. Prefer a meter the subject owns over one I
built. When I must build one, pass the flag that gives it teeth AND write a known-red control
through it once before believing a green.

Related: [[dont-take-a-diagnosis-as-measured]] — my own instrument is a diagnosis too.
