ANTIGRAVITY LABJP
Articles/Tips & Best Practices
Tips & Best Practices/2026-08-21Beginner

The pipe in my wrapper was swallowing Antigravity CLI's exit code

CLI 1.1.14 surfaces language server failures as a non-zero exit. That improvement never reaches you if your automation wrapper drops the exit code first. I ran sixteen calling patterns on bash 5.1 and recorded which ones make the failure disappear, plus the fixes that bring it back.

antigravity-cli11automation89bashci-cd16exit-code

There is a quiet line in the CLI 1.1.14 release notes. When the language server fails at startup or during a run, the CLI used to exit without printing anything. Now the failure is logged to the terminal and the exit code is non-zero.

In other words, failures that used to pass as successes are finally visible.

I read that and went to look at my own scheduled scripts. As an indie developer I run nightly updates for a handful of sites I maintain alone, and those scripts call CLI tooling from bash. If a non-zero exit was now coming back, my morning logs should have picked up some red.

The next morning the logs were exactly as green as the day before.

The improvement had landed. My wrapper simply was not built to receive it.

The way you call a command decides whether its failure survives

An exit code is fixed the moment a command returns. What the shell finally reports, though, depends on the shape of the line you wrote around it.

A pipe is the clearest case.

antigravity-cli run ... | head -1

The exit code of that line belongs to head. No matter what number the left side died with, once head reads one line successfully the result is 0. The error message either went to 2>/dev/null or scrolled past in a log nobody opens.

This is documented bash behaviour, not a bug. But when you are writing an automation wrapper, that behaviour works as an eraser for failures. The wrapper reports a clean run, the scheduler records a success, and the only evidence that anything went wrong sits in a stream you redirected away weeks ago. That gap is easy to live with for months, because nothing ever asks you to look.

I measured sixteen calling patterns instead of guessing

Guessing at the fix seemed like a poor use of a morning, so I measured. I wrote a tiny script that exits with code 1, called it sixteen different ways, and recorded what the shell reported at the end.

The test script is this small:

#!/bin/bash
echo "Starting language server..."
echo "language server exited unexpectedly (code 1)" >&2
exit 1

The environment is GNU bash 5.1.16. Here is what came back.

Calling patternReported exit codeFailure survives?
cmd (direct)1Yes
cmd | head -10No
set -o pipefail with cmd | head -11Yes
OUT=$(cmd) at top level1Yes
RESULT="prefix-$(cmd)"1Yes
if cmd; then ... fiYes (takes the else branch)
cmd | tail -1 inside a set -e script0No
local OUT=$(cmd) inside a function0No
declare OUT=$(cmd) inside a function0No
export OUT=$(cmd)0No
cmd | tee run.log0No
cmd | while read -r l; do :; done0No
cmd && next1Yes (next never runs)
background start plus wait1Yes
timeout 10 cmd1Yes
cmd | head -1 then read ${PIPESTATUS[0]}0 (array holds 1)Yes, if you read it

Seven of the sixteen erased the failure. What stings is that all seven are ordinary, sensible-looking lines. You add tee because you want a log. You add local because you are assigning inside a function. You pipe into while read because you want to handle output line by line. Nothing about them looks careless, and yet the result quietly becomes 0.

Two of the seven were live in my own scripts: tee and local. Neither had been touched in months, and both were sitting on the exact path a language server failure would have taken. That is the part I keep coming back to: the wrapper had been silently downgrading real failures the whole time, and the CLI update simply gave me a reason to notice.

The seven come down to three causes

Once the table is in front of you, the pattern is easy to see.

Cause 1: the right end of a pipe overwrites the result

| head, | tail, | tee, | while read — in every case the right side is the last command in the pipeline, and the shell reports its exit code. The left side's failure lives only in ${PIPESTATUS[0]}, and only if you go read it.

Cause 2: a variable declaration is itself a command that succeeds

local, declare, and export are commands in their own right. Write local OUT=$(cmd) and the shell reports that local ran successfully. Whatever the substitution on the right side exited with is overwritten by that success.

This behaves differently from a plain OUT=$(cmd) at the top level. In my run, the top-level form returned 1 while the local form inside a function returned 0. The two lines look nearly identical and behave in opposite ways, which is where I spent the longest being wrong.

Cause 3: set -e never sees the middle of a pipeline

I had assumed a few scripts were covered because they open with set -e. Testing it, a cmd | tail -1 line inside a set -e script sailed straight through, the following statements ran, and the script finished with 0. set -e reacts to the exit code the shell received. Once the pipe has turned that into 0, there is nothing left to react to.

What I changed, and what the change measured

I applied a fix per cause and re-ran the same test script against each one.

BeforeAfterResult
cmd | head -1add set -o pipefail at the top0 → 1
local OUT=$(cmd)split into local OUT then OUT=$(cmd)0 → 1
cmd | tee run.logcmd > >(tee run.log)0 → 1
cmd | while read ...combine with set -o pipefail0 → 1
set -e aloneset -euo pipefail0 → 1 (later lines stop running)
piping for formattingwrite to a file first, then RC=$?1 (output still kept)

The two-line split for local is the smallest edit with the largest payoff.

# failure disappears
check_agent() {
  local OUT=$(antigravity-cli run --headless "$1")
  return $?
}
 
# failure survives
check_agent() {
  local OUT
  OUT=$(antigravity-cli run --headless "$1")
  return $?
}

Separating the declaration from the assignment means $? now reflects the assignment. Measured, it moved from 0 to 1.

Swapping tee for a process substitution is my favourite of the set, because you keep the log and keep the code.

antigravity-cli run --headless "$TASK" > >(tee run.log) 2>&1
echo "exit=$?"   # now returns 1

One caveat worth knowing. Both set -o pipefail and > >(...) are bash features and are not available in sh (dash). Running sh -c 'set -o pipefail' on my machine returned Illegal option -o pipefail. If your CI runs scripts under /bin/sh, either pin the shebang to #!/bin/bash or fall back to the write-to-a-file approach.

A one-minute audit before you upgrade

Checking the calling side before you bump the CLI means you can actually trust the logs afterwards. Two lines were enough for me.

grep -rn 'antigravity[^|]*|' ./scripts | grep -v pipefail
grep -rn 'local .*=\$(\|declare .*=\$(\|export .*=\$(' ./scripts

The first finds places where output is received through a pipe. The second finds declarations that assign on the same line. If both come back empty, the non-zero exit from CLI 1.1.14 will reach your automation untouched. If they hit, apply the matching row from the table above.

I have moved this audit ahead of the upgrade itself. Fixing what you are monitoring while the monitor is broken leaves you with no way to confirm the fix. I ran into the same category of silent failure from a different direction in When an AI Agent's git push Reports Success but Nothing Reaches the Remote. If you would rather validate the CLI side first, Find the one broken MCP entry before Antigravity starts, with 40 lines of Node covers adjacent ground.

Next time you open an automation script, try running just the first grep. Zero hits is a perfectly good result to get.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Tips2026-06-14
Turning 'the Antigravity CLI feels faster' into a number with hyperfine
The Go-based Antigravity CLI feels snappier to start. Here is how to turn that impression into a reproducible number with hyperfine: warm vs cold runs, cumulative cost in automation, and a CI gate that catches regressions.
Integrations2026-06-13
Running the Antigravity CLI (agy) Headless in CI: Working Around the Non-TTY stdout Problem
Run agy -p inside GitHub Actions or cron and the output you saw locally can vanish, while the exit code still returns 0. Here is how non-TTY detection causes it, plus a robust setup using a pseudo-TTY, defensive text parsing, and API-key auth so you always capture the result.
Tips2026-07-10
You Can Measure a Request Before You Send It — Sizing Agent Tasks by Working Backward from Rework Rate
When an Antigravity agent returns code that misses the mark, the cause is rarely the wording of the prompt. It is the size of the task. Here is a Python scorer that grades a request before you send it, plus what happened when I scored 80 past requests against their actual rework outcomes.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →