The Moment of Insight: Debugging a Bash if ! cmd | tee Gotcha
Introduction
In the high-stakes world of GPU proving for Filecoin, a production crash on an RTX 5090 instance initially appeared to be an out-of-memory (OOM) kill. The daemon was a zombie, the benchmark had stalled, and the natural instinct was to blame memory pressure. But as the investigation unfolded, the assistant uncovered something far more insidious: a subtle bash scripting bug involving set -euo pipefail, the ! negation operator, and a tee pipeline that together produced a runtime syntax error, masked the true exit code, and prevented the timed benchmark phase from ever running. Message [msg 4051] captures the critical turning point—the moment the assistant identified the if ! cmd | tee pattern as the culprit.
This article examines that single message in depth: the reasoning that led to the insight, the assumptions that had to be discarded, the bash semantics that had to be correctly understood, and the knowledge produced by the analysis. It is a case study in how complex shell scripting interactions can produce baffling failures, and how systematic reasoning can untangle them.
Context: A Crash That Wasn't an OOM
The session leading up to [msg 4051] had been a prolonged battle with memory management. The assistant had implemented a PI-controlled dispatch pacer, a synthesis throughput cap, cgroup-aware memory detection, and an OOM recovery loop. Yet the RTX 5090 instance (C.32897009) kept crashing. The daemon was a zombie, and the initial diagnosis pointed to an OOM kill.
But when the assistant examined the benchmark log (/tmp/benchmark-full.log), it found something unexpected: the script had aborted with a "syntax error near unexpected token else" on line 346, after Phase 1 (pipeline warmup) had completed successfully. The daemon wasn't OOM-killed—it was killed by a bash script bug. The assistant had shipped a broken benchmark.sh.
The next several messages ([msg 4034] through [msg 4050]) were a deep dive into the script. The assistant read the file, checked syntax with bash -n (which passed), compared local and remote copies (identical), and traced the control flow. The error message pointed to line 346, but the syntax check passed—meaning the error was a runtime phenomenon, not a parse-time one. The assistant also noticed duplicate output: the "PCE warmup completed" and "PCE file present" lines appeared twice in the log, once from the PCE warmup block and once inexplicably after Phase 1. Something was re-evaluating parts of the script.
The Subject Message: A Breakthrough
Message [msg 4051] begins with the assistant's moment of insight:
OH WAIT. I see it now! Look at lines 359-362:
>
``bash if ! "$BENCH_BIN" --addr "http://$ADDR" batch \ -t porep --c1 "$C1_PATH" \ --count "$WARMUP_PROOFS" --concurrency "$CONCURRENCY" \ 2>&1 | tee /tmp/cuzk-bench-phase1-warmup.log; then ``
The assistant had been staring at this pattern for several messages, but now the implications crystallized. The if ! cmd | tee pattern, when combined with set -euo pipefail, has a critical semantic problem.
The assistant walks through the reasoning step by step. First, it considers how ! interacts with pipelines:
Withset -euo pipefail: -! cmd | tee— the!only negates the first command in the pipeline. Actually no,!negates the entire pipeline's exit status. - But withpipefail, the pipeline's exit code is the rightmost failed command.
This self-correction is important. The assistant initially misremembered the semantics—thinking ! might only apply to the first command—but immediately corrected itself. In bash, ! negates the exit status of the entire pipeline, not just the first command. With pipefail enabled, the pipeline's exit status is the exit status of the last command that fails (or zero if all succeed).
The assistant then works through the logical consequence:
Withpipefail,! cmd | tee filein anifstatement: - Ifcmdexits 0 andteeexits 0: pipeline exit = 0, negated to 1,ifbody runs (ERROR: enters error handler even though benchmark succeeded)
>
Wait, that's the bug!if !negates success into failure! When the Phase 1 batch succeeds (exit 0),! (batch | tee)becomes exit 1, so thethenbranch runs.
This is the core insight. The if ! pattern was designed to detect when the benchmark command fails—the intent was "if the benchmark fails, handle the error." But because ! negates the exit status, a successful benchmark (exit 0) becomes exit 1 in the if condition, causing the then branch (the error handler) to execute. Conversely, if the benchmark actually fails (exit non-zero), ! negates it to 0, and the then branch is skipped.
This is a classic gotcha with ! in bash: it inverts the logic you'd naturally write. The correct pattern would be either if cmd | tee log; then (without !) to handle success, or if ! cmd | tee log; then only if you want to handle failure—but the exit code inversion makes the error handler run on success.
The Contradiction: Observed Output vs. Predicted Behavior
But the assistant immediately hits a contradiction. If the if ! bug were the whole story, the output should show the error handler messages: "ERROR: pipeline warmup failed" and "Benchmark failed (non-OOM exit=1)". Yet the log shows something different:
But the output shows that Phase 1 DID complete normally and output the batch summary. Thenrun_benchmarkreturned 1 (from line 368). Back in the OOM loop,if run_benchmarkfails, we go torc=$?(which is 1). Then$rc -ne 137is true, so it says "Benchmark failed (non-OOM exit=$rc), not retrying."
>
But wait — the output doesn't show "ERROR: pipeline warmup failed" or "Benchmark failed" — it shows the duplicate PCE lines and a syntax error. So something else is happening.
This is the hallmark of good debugging: the assistant doesn't stop at the first plausible explanation. It tests the hypothesis against the observed evidence and finds a mismatch. The predicted output (error messages) doesn't match the actual output (duplicate PCE lines and a syntax error). Something else is going on.
The assistant re-examines the log output:
Actually... let me re-read the log output. After the batch summary: ``PCE warmup completed in 206s PCE file present: /var/tmp/filecoin-proof-parameters/pce-porep-32g.bin (26G) /usr/local/bin/benchmark.sh: line 346: syntax error near unexpected tokenelseStopping daemon (PID=1624)...``
>
The "Stopping daemon" message comes from the cleanup trap. The syntax error killed the script. But where are the repeated "PCE warmup" lines coming from?
The duplicate "PCE warmup completed" and "PCE file present" lines are the key anomaly. These lines come from lines 342-346 of benchmark.sh, which are inside the PCE warmup block—a block that should have executed before Phase 1, not after. Their reappearance after Phase 1 suggests that something is causing the script to re-execute or re-evaluate that section.
The message ends with the assistant investigating the trap handler:
Let me check if there's a trap that echoes cleanup and runs parts of the script: [grep] trap|cleanup|stop_daemon Found 2 matches /tmp/czk/docker/cuzk/benchmark.sh: Line 156: cleanup() { Line 164: trap cleanup EXIT
The Thinking Process: Iterative Hypothesis Refinement
What makes [msg 4051] so instructive is the visible thinking process. The assistant doesn't jump to conclusions. It proposes a hypothesis (the if ! inversion bug), tests it against the evidence, finds a contradiction, and refines the investigation. This is the scientific method applied to debugging.
The assistant's reasoning can be broken down into several stages:
- Pattern recognition: The assistant spots the
if ! cmd | teepattern and recognizes it as potentially problematic. - Semantic analysis: It works through the bash semantics of
!,pipefail, and pipeline exit codes, correcting its own initial misunderstanding. - Hypothesis formation: It concludes that
if !inverts the success/failure logic, causing the error handler to run on success. - Evidence testing: It checks whether the predicted output matches the observed log, and finds a mismatch—the error handler messages are absent.
- Contradiction resolution: Rather than discarding the hypothesis entirely, the assistant reframes the question: if the
if !bug isn't producing the expected error messages, what is producing the duplicate PCE lines and syntax error? - New direction: The assistant pivots to investigating the trap handler, suspecting that the cleanup function might be re-executing parts of the script. This iterative refinement is the essence of effective debugging. Each cycle narrows the space of possible explanations and produces new questions to investigate.
Assumptions and Their Corrections
Throughout the investigation, several assumptions were made and later corrected:
Assumption 1: The crash was an OOM kill. The initial evidence (zombie daemon, memory pressure) pointed to OOM. But the log revealed a bash syntax error. The assistant had to discard the OOM hypothesis and look at the script itself.
Assumption 2: The syntax error was a parse-time error. The error message said "syntax error near unexpected token else," which typically indicates a parse failure. But bash -n passed, proving the syntax was valid. The error had to be a runtime phenomenon—something about the execution context caused bash to encounter an unexpected token.
Assumption 3: The if ! bug fully explained the failure. The assistant initially thought the if ! inversion caused the error handler to run on success. But the missing error messages contradicted this. The if ! pattern was part of the story, but not the whole story.
Assumption 4: The duplicate output came from some replay mechanism. The assistant considered whether tee was replaying its log file, or whether a subshell was re-evaluating code. The pivot to the trap handler suggests the assistant now suspects the cleanup function (triggered by trap cleanup EXIT) might be the source.
Input Knowledge Required
To fully understand [msg 4051], the reader needs:
- Bash
set -euo pipefailsemantics: Understanding thatset -emakes the shell exit on errors,set -utreats unset variables as errors,set -o pipefailmakes a pipeline's exit code reflect the rightmost failed command, and all three together create a strict execution environment. - Bash
!negation: The!operator negates the exit status of the entire pipeline, not just the first command.! (exit 0)yields exit 1, and! (exit 1)yields exit 0. - Bash
ifstatement exit codes: After anifblock,$?reflects the exit status of theifcondition itself (0 if thethenbranch ran, 1 if theelsebranch ran), not the exit status of the command inside the condition. - Bash
teebehavior:teewrites stdin to both stdout and a file, and its exit code reflects its own success (typically 0 unless disk write fails). - Bash trap handlers:
trap cleanup EXITregisters a function to run when the shell exits, regardless of whether the exit is normal or due to an error. - The structure of
benchmark.sh: The script has arun_benchmarkfunction with three phases (PCE warmup, pipeline warmup, timed run), wrapped in an OOM retry loop. The Phase 1 warmup uses theif ! cmd | teepattern.
Output Knowledge Created
Message [msg 4051] produces several pieces of knowledge:
- The
if ! cmd | teepattern is buggy underpipefail. The!negation inverts the success/failure logic, causing the error handler to execute when the command succeeds. This is a concrete, actionable finding that can be fixed by removing the!or restructuring the condition. - The duplicate output and syntax error are not explained by the
if !bug alone. Something else is causing the PCE warmup block to re-execute and producing the syntax error. The trap handler is a suspect. - The syntax error is a runtime phenomenon, not a parse-time one. The script passes
bash -nbut fails at runtime, meaning the error depends on execution context—perhaps a function being evaluated in the wrong scope, or a trap handler re-evaluating code that assumes certain variables are set. - The OOM recovery loop has a separate bug in exit code capture. The assistant noted earlier (in [msg 4050]) that
rc=$?after anifblock always yields 0 or 1, not the actual exit code ofrun_benchmark. This means OOM exits (137) are never detected, and the retry loop never triggers.
The Broader Significance
Message [msg 4051] is a microcosm of what makes debugging complex systems so challenging. The failure manifests as a syntax error, but the root cause is a chain of interacting factors: the if ! inversion, the pipefail exit code semantics, the trap handler, and the OOM retry loop's exit code capture bug. Each factor alone might be benign, but together they produce a catastrophic failure that masks the true cause.
The assistant's approach—forming hypotheses, testing them against evidence, and refining—is the right methodology. The message also illustrates the importance of reading error messages literally ("syntax error near unexpected token else") while also questioning what they imply (if it's a runtime error, what runtime condition triggers it?).
For anyone debugging bash scripts in production, this message is a cautionary tale about the if ! cmd | tee pattern. It's a pattern that looks correct—"if not (command succeeds), handle error"—but under pipefail, it does the opposite. The fix is to either remove the ! and restructure the logic, or use a different pattern like cmd | tee log; if [ ${PIPESTATUS[0]} -ne 0 ]; then to check the actual exit code of the command, not the pipeline.
Conclusion
Message [msg 4051] captures the moment when a confusing crash finally starts to make sense. The assistant identifies the if ! cmd | tee bug, recognizes that it doesn't fully explain the observed behavior, and pivots to investigate the trap handler. The message is a testament to the value of systematic reasoning, hypothesis testing, and the willingness to admit when a hypothesis doesn't fit the evidence.
The debugging journey doesn't end here—the assistant will go on to fix the script, deploy the fix, and eventually discover that the underlying memory pressure remains a critical issue. But [msg 4051] is the turning point where the bash script bugs are unmasked, separating the scripting errors from the genuine memory management challenges. It's a reminder that in complex systems, the most obvious explanation (OOM kill) is not always the right one, and that the tools we use to manage our systems (bash scripts with strict error handling) can themselves become sources of failure.