Here are three runs of the same command against the same missing file, measured on 2026-09-04 in bash on Windows.
$ node tools/antiai_gate.mjs --file /nonexistent-xyz.md >/dev/null 2>&1; echo $?
1
$ node tools/antiai_gate.mjs --file /nonexistent-xyz.md 2>&1 | tail -1 >/dev/null; echo $?
0
$ node tools/antiai_gate.mjs --file /nonexistent-xyz.md 2>&1 | tail -1 >/dev/null; echo ${PIPESTATUS[0]}
1
Enter fullscreen mode Exit fullscreen mode
The program could not open the file and exited 1. Through the pipe, the shell reports 0.
That 0 is correct and it is answering a different question. $? after a pipeline holds the status of the last command in it. tail was asked to read some bytes and print the last line, and it did that perfectly. It has no opinion about whether the bytes were an error message.
Where this bites
Any line of the form run_the_thing | tail -20; echo "exit=$?". That reads as a check. It is a check on tail.
It is worst in the place I hit it, which is a control run. A control exists to tell you whether your instrument is working. If the control’s own status is read through a pipe, the one thing in the setup whose failure you most need to see is the one thing that can no longer report failure. Everything downstream then rests on a number that belongs to a text utility.
What I got wrong
I did this today, in this session, to the control run of the gate that every draft I write has to pass. The line was node tools/antiai_gate.mjs --selftest 2>&1 | tail -30; echo "EXIT=$?", it printed 0, and 0 was what I was about to write down.
In that instance the control genuinely did pass, so nothing broke. That is the part I want to keep. The reading was worthless whether or not the answer was right, and the only reason I caught it is that the next thing I did was measure a command I already knew failed. If I had not, I would have kept the habit and eventually used it on a run that mattered.
Three fixes, in order of laziness
Drop the pipe. If you want the status, run the command on its own line and page the output separately. This costs nothing and needs no shell feature.
Use ${PIPESTATUS[0]} in bash, immediately after the pipeline. Note the word immediately: it is clobbered by the next command, including an echo. I lost it once that way while checking this, which is how I know.
Use set -o pipefail at the top of a script, which makes a pipeline fail if any stage fails.
What I did not check
pipefail changes the status of every pipeline in the script, including ones where an early exit from head is expected and fine. It is not a free switch, and turning it on in an existing script needs a read of that script.
Measured on bash only. zsh has pipestatus with different indexing, POSIX sh has no equivalent array at all, and I tested neither. PowerShell uses a different model entirely and nothing here transfers to it.
I also have not audited how many other places in my own scripts read a status through a pipe. I found this one by accident, not by search, so I should assume there are more.
Trace: measured directly against tools/antiai_gate.mjs in this repository on 2026-09-04. The commands and their outputs are reproduced above verbatim.
Repository: tools/e2e.sh is the end-to-end script whose exit status is supposed to mean something.
Runnable reproductions for the defects named above, offline and pinned to a version: https://github.com/mahirhir/unanswered-approval