◉ANTIGRAVITY LABJP
Articles/Integrations
⬡ Integrations/2026-06-21Advanced

Piping Antigravity CLI's JSON Lines Output Into My Own Script — Notes on Partial Lines and Exit Codes

Feeding Antigravity CLI's machine-readable JSON Lines output into my own script broke with JSONDecodeError roughly one run in three. Here are the three traps — partial lines, exit codes, and stderr bleed — and the line-buffered consumer that finally made it stable.

antigravity472cli7automation96jsonpipeline5

✦ Premium Article

I wired up a small pipeline: take the output of the Antigravity CLI (agy), run it through a quality check, and only then hand it to the next step. For the first few days it died with json.JSONDecodeError: Expecting value about one run in three. The agent itself was running fine — it was the code receiving its output that broke. The cause wasn't the agent or the model. It was how I was reading the stream.

You never notice this while watching the CLI interactively. It only surfaces the moment you start consuming the output by machine. Since plenty of people hit the same wall, let me walk through what was actually happening and how I fixed it.

Machine-readable output is line-based, but lines arrive in pieces

Separate from its human-friendly display, the Antigravity CLI can emit a machine-readable stream of events. The flag name varies by version (check agy --help), but it's usually JSON Lines (NDJSON) — one JSON object per line. Events like agent_started, tool_call, and agent_completed stream out a line at a time as the agent progresses.

My first misconception lived right here. "If it's one JSON per line, I'll just split the blob on newlines and call json.loads." That works fine if you receive everything in one batch. The trouble began the instant I started reading the stream incrementally, because I wanted to watch progress live.

Pipes and streams make no promise about "lines." If you read in fixed-size chunks like read(4096), you get a blob that's cut off mid-line. That line stays incomplete until the rest arrives on the next read. The code I first wrote stepped right off that assumption.

import json
import subprocess
 
proc = subprocess.Popen(
    ["agy", "run", "--json", "task.md"],
    stdout=subprocess.PIPE,
    text=True,
)
 
# ❌ a single read can hand you only the first half of a line
while True:
    chunk = proc.stdout.read(4096)
    if not chunk:
        break
    for line in chunk.split("\n"):
        event = json.loads(line)  # partial / blank lines raise JSONDecodeError
        handle(event)

The last element of chunk.split("\n") is usually the front half of a line that belongs with the start of the next chunk. Pass it to json.loads on its own and it breaks, of course. Blank lines (the empty string produced by a trailing newline) fail the same way. Because the error showed up probabilistically, I first blamed the model for "occasionally emitting junk" and took the long way around before reaching the real cause.

The simplest correct consumer iterates one line at a time

A Python text-mode stream guarantees complete lines when you iterate it with for line in stream:. If you'd rather not think about partial chunks at all, moving to this shape is the shortest path. While I was at it, I built in line buffering (bufsize=1) and stderr separation from the start.

import json
import subprocess
import sys
 
 
def consume(cmd):
    """Consume the CLI's JSON Lines output one safe line at a time."""
    proc = subprocess.Popen(
        cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,  # keep progress logs out of stdout
        text=True,
        bufsize=1,               # line buffering
    )
 
    events = []
    for raw in proc.stdout:      # one line at a time, always complete
        line = raw.strip()
        if not line:             # skip blank lines
            continue
        try:
            events.append(json.loads(line))
        except json.JSONDecodeError:
            # non-JSON lines (startup banners, etc.) get dropped, but recorded
            sys.stderr.write(f"skip non-json line: {line[:120]}\n")
 
    code = proc.wait()
    return code, events

for raw in proc.stdout: is the key. That alone makes the partial-line problem disappear. We strip() whitespace and newlines, drop blank lines, and discard non-JSON lines while logging them to stderr. If you swallow the exception with a bare pass, you fall into a different swamp later — "why are some events missing?" Even when you drop a line, leave a trace that you dropped it. That one is a note to myself.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦You'll be able to stop JSON parsing from breaking on lines that arrive half-finished, by buffering on line boundaries and carrying the remainder forward
✦You'll learn a dual-gate design that decides success from both the exit code and a completion event, instead of trusting the last line you happened to read
✦You'll walk away with a consumer template ready for unattended runs — stdout/stderr separation, timeouts, and a fresh working directory per run
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
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 →

Related Articles

⬡ Integrations2026-11-03
Turning a Spreadsheet Chore into Apps Script: The First Request a Non-Engineer Can Hand to Antigravity
For non-engineers who want a weekly copy-and-paste routine handled by Apps Script: how to word the request to Antigravity, build in a dry run, keep the data safe, set a trigger, and sort out the usual snags.
⬡ Integrations2026-10-17
When Antigravity's Browser Agent Stops at the Login Screen: Four Things to Check
The browser agent came back with only a sign-in page. Here are four checks, built on the fact that it runs in a separate Chrome profile, to get its login state sorted.
⬡ Integrations2026-09-12
Antigravity CLI 1.1.28 turns a print-timeout overrun into a successful exit — rechecking unattended runs
Since 1.1.28, an expired --print-timeout returns partial output and exits successfully instead of failing. Fetching an external URL now asks for approval first. Here is how I rebuilt an unattended pipeline that judged everything by the exit code.