---
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-22T09:31:32.742Z
---

`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.
