It Worked Interactively but Went Silent Overnight — Making an Antigravity Agent Behave the Same in the Desktop and the CLI
An agent that runs perfectly in the Antigravity desktop app but does nothing when you schedule it through the CLI. This walks through absorbing the gap between interactive and unattended runs across four points — approvals, context, secrets, and runtime — with working code and a preflight check, so one definition behaves identically on both.
On a Friday night I took an agent that ran flawlessly on my desktop and dropped it, unchanged, into a scheduled run. The next morning the log showed nothing — no commit, no push, not even an error. It had quietly timed out waiting for an approval dialog that could never appear.
This gets easier to hit now that Antigravity 2.0 has split into a VS Code–style IDE and a chat-style agent app, and the surfaces have multiplied further with the Antigravity CLI (agy) and cloud runs via the Gemini Enterprise Agent Platform. The moment an agent definition assumes an interactive run, it stops holding up unattended. As an indie developer I run several blog and app updates in parallel — interactively during the day, and through a scheduled CLI at night — so I have tripped over this "surface gap" more than once.
Forking the definition per surface is the wrong fix: patch one and the other rots. The goal here is to keep a single definition and absorb the interactive/unattended difference at four points, with working code.
The gap breaks on assumptions, not on speed
When an unattended run fails, the cause is rarely the model's intelligence or latency. It's almost always an assumption a human filled in silently during an interactive run that simply isn't there off the terminal. In my experience the gap collapses to four places.
Break point
Interactive (desktop)
Unattended (CLI / scheduled / cloud)
Approvals
A human decides in the approval dialog
No dialog can show; it waits silently, then dies
Context
@ references, the selection, the open folder flow in implicitly
None of that exists; only the string you passed is context
Secrets
Pulled interactively from the keychain
Keychain is locked; only env vars or secret files
Runtime
The interactive shell's PATH and working folder apply
A minimal PATH; working directory undefined
The important move is not to rewrite the definition so it "also works unattended," but to detect the surface in one place and insert a layer that absorbs the per-surface difference. The definition itself reads the same on either surface.
Detect the surface in exactly one place
Scatter the check and interactive-only branches spread everywhere until it breaks. Decide once at the entry point and carry it through an environment variable.
# surface.sh — detect the run surface once and carry it through# If stdin and stdout are attached to a terminal, treat it as interactiveif [ -t 0 ] && [ -t 1 ] && [ -z "${AGY_SCHEDULED:-}" ]; then export AGY_SURFACE="interactive"else export AGY_SURFACE="unattended"fiecho "surface=$AGY_SURFACE"
When you launch from a scheduler or the CLI, also pass AGY_SCHEDULED=1 so the run is unambiguously unattended. Relying on the terminal check alone can misclassify a piped interactive case, and that one line of insurance is what makes the approval policy below fail safe.
✦
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 pin down why an agent that worked in the desktop app silently stalls under the CLI or a scheduled run, by splitting the causes into approvals, context, secrets, and runtime
✦You'll add surface detection, an approval-policy shim, and a fixed secret-resolution order so a single agent definition behaves the same interactively and unattended
✦You'll wire a preflight parity check before scheduling, so an overnight unattended run doesn't surface as a morning incident
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.
Approvals: switch between "ask a human" and an allowlist
The most common unattended failure is a destructive action that a human would have approved interactively, stalling because nobody can approve it. But auto-approving everything unattended removes the brakes entirely. The answer is to switch who decides by surface.
// approval-policy.js — ask a human when interactive, use an allowlist when unattendedconst ALLOW = new Set([ "read_file", "search_repo", "run:npm test", "git:add", "git:commit",]);// On the unattended surface, only explicitly allowed actions pass; everything else stopsfunction decide(action, { surface }) { if (surface === "interactive") return "ask"; // a human makes the final call if (ALLOW.has(action.key)) return "allow"; // only pre-approved actions auto-run return "stop"; // anything unexpected halts}module.exports = { decide };
I deliberately keep git:push and deploy off the allowlist, because for an unattended run "stop and notice it in the morning" is safer than "run on its own and break something overnight." An allowlist that's a little too tight is exactly right; loosen it only after you've watched it behave. For the broader thinking on overnight brakes, see designing guardrails when you hand overnight work to a Background Agent.
Secrets: fix the resolution order
An agent that only ever ran interactively is usually pulling tokens from the keychain — which an unattended run cannot open. Rather than panic and hardcode a secret, fix the resolution order so there's always one path that holds up unattended.
# resolve-secret.sh — fix the secret resolution order# env var -> secret file -> (interactive only) keychainresolve_secret() { local name="$1" # 1) environment variable (first choice for unattended) if [ -n "${!name:-}" ]; then printf '%s' "${!name}"; return 0; fi # 2) secret file (keep the dir outside the repo, .gitignore it) if [ -f "${AGY_SECRETS_DIR:-/nonexistent}/$name" ]; then cat "${AGY_SECRETS_DIR}/$name"; return 0 fi # 3) keychain (interactive only; unattended never reaches here) if [ "$AGY_SURFACE" = "interactive" ]; then security find-generic-password -a "$USER" -s "$name" -w 2>/dev/null && return 0 fi echo "MISSING_SECRET:$name" >&2 return 1}
Fixing the order matters because it guarantees the same name returns the same value on both surfaces. If interactive pulls a fresh value from the keychain while unattended grabs a stale env var, that drift will burn hours of debugging. Once I standardized on a single resolver, time lost to secret-related debugging dropped noticeably.
Context: don't rely on the implicit; hold it in the definition
On the desktop, the open files, the current selection, and anything you referenced with @ flow into context implicitly. The unattended surface has none of that. So pin the context the agent needs as files in the repository, not as IDE state.
# context.sh — assemble context from explicit files so it holds up unattended: "${AGY_WORKDIR:?fix the working directory first}"cd "$AGY_WORKDIR"CONTEXT_FILES=( "docs/agent/TASK.md" # the task's goal and done conditions "docs/agent/CONVENTIONS.md" # naming, formatting, and other agreements)CONTEXT=""for f in "${CONTEXT_FILES[@]}"; do if [ -f "$f" ]; then CONTEXT+=$'\n\n# '"$f"$'\n'"$(cat "$f")" else echo "context file not found: $f" >&2; exit 1 fidoneexport AGY_CONTEXT="$CONTEXT"
Even with these absorbing layers in place, a missing env var or a stripped PATH at scheduling time will still die silently overnight. So put one preflight check that every registration must pass.
#!/usr/bin/env bash# preflight.sh — before scheduling, verify the unattended surface's assumptions holdset -euo pipefailfail=0need() { command -v "$1" >/dev/null 2>&1 || { echo "$1 not on PATH"; fail=1; }; }# 1) commands used unattended are found even on a minimal PATHneed node; need git; need rg# 2) required secrets resolve from env or a secret file (not keychain-only)for s in GEMINI_API_KEY DEPLOY_TOKEN; do if [ -z "${!s:-}" ] && [ ! -f "${AGY_SECRETS_DIR:-/nonexistent}/$s" ]; then echo "secret $s cannot be resolved on the unattended surface"; fail=1 fidone# 3) the working directory is fixed (not the IDE's "open folder")if [ -z "${AGY_WORKDIR:-}" ] || [ ! -d "${AGY_WORKDIR:-/nonexistent}" ]; then echo "AGY_WORKDIR unset or missing"; fail=1fi# 4) the approval policy loads for the unattended surfacenode -e "require('./approval-policy.js').decide({key:'git:push'},{surface:'unattended'})" \ >/dev/null 2>&1 || { echo "cannot load approval-policy.js"; fail=1; }if [ "$fail" = 0 ]; then echo "OK: no surface gap; safe to run unattended"else echo "STOP: fix the above before scheduling"; exit 1fi
Since I made this preflight part of the registration steps, the "it launched overnight but did nothing" misfires that used to hit me two or three times a week dropped to roughly once a month in my environment. More than the number, what helped is that failures moved forward to a red light before registration. Fixing something on the spot is far cheaper than discovering it as a morning incident.
Where this tends to trip you up
A few pitfalls I actually hit.
First, don't decide the surface from the terminal check alone. A pipe or redirect can make an interactive run look unattended, and a wrapper can make an unattended run look attached to a terminal. Pair it with an explicit flag like AGY_SCHEDULED.
Second, don't loosen the approval policy to "allow everything unattended." An unattended run with no brakes does outsized damage once it misbehaves, and overnight it's found late. Start the allowlist narrow and widen it only after you've watched it.
Third, don't count on a PATH that only ever passed interactively. The unattended PATH is minimal. Make the commands your agent calls absolute, or verify their presence in the preflight. Related command not found stalls usually resolve once you revisit PATH inheritance on the runtime surface.
Your next move
Pick just one agent you currently run interactively, push it through surface.sh and preflight.sh, and do a dry unattended run. Where it stops — approvals, context, secrets, or runtime — is exactly the assumption interactive was filling in silently. I'm still adding surfaces myself, but keeping the definition singular pays off more the more surfaces you have.
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.