Running Antigravity CLI Headless: Design Before It Hits CI and cron
The Antigravity CLI was rewritten in Go. Here is how to run it unattended in CI and cron, covering exit codes, idempotency, timeouts, and output parsing.
On June 18, Gemini CLI and Gemini Code Assist stop serving individual users, and the successor is the Antigravity CLI. The new binary is rewritten in Go, so it starts and responds noticeably faster. At your terminal, it is close to a drop-in replacement.
The trouble starts after that. I run four sites from a single machine, with agent runs wired into cron and CI. The smoother a tool feels interactively, the more quietly it stalls when nobody is watching. A confirmation prompt waits for an answer that never comes, and the job sits there until it times out. As an indie developer running everything myself, I have hit that exact failure more than once.
This article covers the gap between "it worked interactively" and "I trust it unattended," narrowed to four points: exit codes, idempotency, timeouts, and output parsing.
Why an interactive-first CLI freezes inside CI
Agent CLIs are built to ask humans for confirmation. "Overwrite this file?" "Commit now?" At a terminal you press Enter and move on.
A CI runner or cron has nobody to press Enter. Because stdin is not a TTY, some tools auto-answer "no" and abort, while others wait forever and hang. The second case is the dangerous one: the job log shows nothing while billable minutes drain away.
The first job is to remove every chance of a prompt.
# Auto-approve every confirmation and empty stdin so a human wait is physically impossibleantigravity run \ --prompt-file ./tasks/update-articles.md \ --yes \ --no-color \ --output json < /dev/null
--yes (the auto-approve flag; confirm its exact name with antigravity --help for your version) skips confirmations, and < /dev/null empties stdin. Even if you miss a flag, empty input means it cannot wait. That redundancy is what holds. --no-color keeps ANSI escapes out of the log so downstream parsing stays clean.
Building idempotency that survives a double launch
Unattended jobs will overlap. A previous run drags on while the next cron fires. CI retries and processes the same commit twice. That overlap produces double commits to the same file, or a push of half-finished state.
I handle it in two layers: mutual exclusion of the run itself, and idempotency of the result.
Exclusion comes from a lock file.
LOCK="/tmp/antigravity-articles.lock"# flock prevents concurrent launches. -n means do not wait, give up immediatelyexec 9>"$LOCK"if ! flock -n 9; then echo "Another run is in progress; skipping" exit 0fi# Only one passes this point at a timeantigravity run --prompt-file ./tasks/update-articles.md --yes --output json < /dev/null
Result idempotency comes from making the output target unique per run. Appending to a fixed filename invites contamination from a previous run's leftovers, so I avoid it. I write to a unique name that includes the slug, verify it, then move it into place. In my own Dolice workflow, adding that one step is what made half-written commits disappear.
✦
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
✦How to stop an unattended run from hanging on a hidden prompt, using --yes plus blocked stdin
✦A lock-file and idempotency-key pattern that survives a double-fired trigger without corrupting state
✦A 30-line wrapper that judges success from both exit code and JSON output, and alerts only on failure
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.
Agents occasionally think for a long time. If you leave it to the CI global timeout, the forced kill leaves the agent's partial state behind. So hold the clock from outside.
# Cut off at 10 minutes; on cutoff send SIGTERM, then SIGKILL after 15 secondstimeout --signal=TERM --kill-after=15 600 \ antigravity run --prompt-file ./tasks/update-articles.md --yes --output json < /dev/nullrc=$?if [ "$rc" -eq 124 ]; then echo "Cut off by timeout"fi
The point is not to reach straight for SIGKILL. Send SIGTERM first and give the agent a moment to clean up. I learned this in production after skipping --kill-after: a half-written file survived, and the next run treated it as authoritative.
Judge success from exit code and JSON together
The value of an unattended run is that it reaches a human only when it fails. If success and failure flow into the same log, nobody reads it.
The Antigravity CLI returns structured results with --output json. Judge in two stages, combined with the exit code.
out=$(timeout 600 antigravity run \ --prompt-file ./tasks/update.md --yes --output json < /dev/null)rc=$?# Treat as failure if exit code is non-zero OR JSON status is not successstatus=$(printf '%s' "$out" | jq -r '.status // "unknown"')if [ "$rc" -ne 0 ] || [ "$status" != "success" ]; then printf '%s' "$out" | jq -r '.error // "(no error field)"' >&2 exit 1fi
Do not rely on the exit code alone, because a tool can return 0 for "the run finished but the goal was not met." Do not rely on the JSON alone either, because you would miss the case where the CLI itself crashed and emitted no JSON. Look at both, and call it red if either is red.
Pass credentials without leaking them to the log
Credentials cause the most incidents in CI. Writing an Antigravity API key or a repo token directly on the command line leaves it in the process list and the logs.
# Pass via environment variable, never as an argument. Harder to leak even under set -xexport ANTIGRAVITY_API_KEY="$(cat /run/secrets/antigravity_key)"antigravity run --prompt-file ./tasks/update.md --yes < /dev/nullunset ANTIGRAVITY_API_KEY
Read the key from a mounted file (/run/secrets/), put it in an environment variable, and pass it that way. Never as an argument. I once found a token sitting in plaintext in a GitHub Actions log and had to rotate it on the spot. Since then I wrap credential lines in set +x whenever a script uses set -x.
A staged path to going unattended
Do not drop everything onto cron at once. Acclimate in this order.
Run it by hand with --yes --output json < /dev/null and confirm it finishes with no prompts
Write a wrapper that adds timeout and flock, then run it by hand again
Add the two-stage exit-code and JSON check, force a failure, and confirm the alert fires
Put it on a manual CI trigger and visually scan the log for leaked credentials
Move it to cron, and watch the log daily for the first week
Skip these stages and you usually get burned at step 4 or 5. Going unattended is faster in the end when you build "noticing breakage" before you build "it works."
As a next step, pick the single smallest repetitive task you have and try only step 1, the manual headless run. Wherever it stalls is your environment telling you its own design problem.
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.