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.

antigravity383cli6automation57jsonpipeline5

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-06-18
On Cutover Day to the Antigravity CLI, Verify Production Automation by Side-Effect Equivalence, Not Output
On the day you switch from the Gemini CLI to the Antigravity CLI, verify production automation by the equivalence of side effects — files written, commits, network calls — instead of matching stdout. A sandbox parallel run and a go/no-go cutover gate, with implementation steps.
Integrations2026-04-09
Antigravity × Notion API Integration: AI-Powered Document-Driven Development
Learn how to connect Antigravity IDE with the Notion API to automate your document-driven development workflow — from spec to code, tests, and PR descriptions — using MCP servers.
Integrations2026-04-04
Automating Your Antigravity Development Workflow with n8n and Google AI Studio
Learn how to combine n8n's no-code automation with Google AI Studio's Gemini API to intelligently streamline your Antigravity development process — including PR reviews, error analysis, and more.
📚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 →