The Day I Stopped Splitting by Skill — Three Questions for Routing Work Between Antigravity CLI and Claude Code
An unattended job sat waiting on an approval prompt for three days without a single error line. Here is how I stopped dividing work between two agent CLIs by capability, and started dividing it by where the approval boundary falls.
My pre-release check for one of my wallpaper apps returned three days of empty logs. No failure lines. Exit code zero. And yet the diff report I expected simply was not there.
I found it at the terminal, not in the log. Running the same job by hand produced one line — Allow access to this URL? — and there it sat. While the job ran unattended, nobody was there to see that line.
The trigger was Antigravity CLI 1.1.28, released on September 9, 2026. In that version, fetching an external URL prompts for approval by default unless access has been pre-granted. The night before, I had happened to move this particular check over to Antigravity CLI. The release and my migration landed in the same week.
What I want to say first is that neither the release nor the migration was the real problem. The problem was that I had been splitting two tools by what each was good at.
When I sent the hard jobs to the smarter tool
For a long time I divided my two agent CLIs by capability. Design conversations and cross-cutting cleanup went to Claude Code; repeatable, well-defined procedures went to Antigravity CLI. As an indie developer I only have one pair of hands, so "hard things to the smarter one" looked reasonable at the time.
That split never breaks while you are sitting in front of the screen. If something asks for approval, you click it. It breaks the moment you run the same work unattended.
For a while I blamed my own prompts. My instructions must be vague, I thought, so the agent is hesitating somewhere in the middle — and I rewrote the task files in finer and finer detail. It did not help. However I rewrote them, the same jobs stalled in the same place, and the others never stalled once.
The difference was not difficulty. Every job that stalled was reaching outside the repository.
In 1.1.28, the stopping point moved in two directions at once
This is the part that ran against my intuition. The same release made one class of approval stop blocking, and another class start blocking. Both items sit in the September 9 entry of the Antigravity changelog.
Change
Effect on unattended runs
Fixed headless (-p) runs hanging indefinitely on interactive implementation-plan approval prompts, by proceeding through plan review automatically in non-interactive mode
Stopped blocking. Plan approval used to be the single biggest stall
Changed default URL fetching permissions to prompt for approval before reading external URLs, unless access has been pre-granted
Started blocking. Every job that reads a URL is in scope
If you skim the release headline, this looks like a headless optimization release. And it is one: runs exit faster after the final answer, and up to 200 ms of idle delay per turn was removed. A line that tightens a default permission is sitting quietly in the middle of a list of improvements.
The reason three days went by is that I read "harder to stall" and missed that the stalling point had simply swapped places.
✦
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 will be able to route agent work by where the approval boundary falls, instead of guessing which tool is smarter
✦You will be able to spot a job that will stall silently before it runs unnoticed in production for three days
✦You will know exactly which part of your success check to rewrite when a release moves the stopping point
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.
Looking further back through the changelog for a better split, I found the answer in an older release. In 1.1.20, the default review mode began granting workspace-scoped read access automatically, so reading or listing files in the workspace no longer asks every time — while strictly keeping confirmation for modifications and external access.
So the product had drawn its line long before I went looking. The line is not "hard versus easy." It is "inside the workspace versus outside it."
My capability split ran perpendicular to that line. Sort the same work along two perpendicular axes and something will disagree somewhere. In my case, the disagreement happened to surface in unattended runs.
Route work by whether it reaches outside the workspace, not by how hard it looks. Since rewriting the rule that way, almost all of my hesitation about routing has gone.
Here are the three questions I now use. The headings below follow this order, though the order in which I actually apply them gets rearranged later on.
Does this work touch anything outside the workspace?
Will I notice when it stalls?
What does a redo cost?
Question 1: Does this work touch anything outside the workspace?
In my own setup, "outside" turned out to mean three things: fetching an external URL, pulling a new dependency, and reading a credential. Any of them can trigger an approval prompt, so a job that does them needs pre-granted access before it goes to the unattended side.
I check this mechanically after writing a task file, because reading them by eye guarantees a miss.
#!/usr/bin/env bash# scan-external-reach.sh — find every reach outside the workspace in a task file# usage: ./scan-external-reach.sh tasks/*.mdset -euo pipefailPATTERN_URL='https?://|curl |wget |ReadURL|fetch\('PATTERN_DEP='npm i |npm install|pip install|brew install|go get |cargo add'PATTERN_CRED='API_KEY|_TOKEN|_SECRET|credentials|\.env'exit_code=0for f in "$@"; do hits="" grep -qE "$PATTERN_URL" "$f" && hits="${hits} url" grep -qE "$PATTERN_DEP" "$f" && hits="${hits} dependency" grep -qE "$PATTERN_CRED" "$f" && hits="${hits} credential" if [ -n "$hits" ]; then echo "OUTSIDE ${f} →${hits}" exit_code=1 # a human looks once before this goes unattended else echo "INSIDE ${f}" fidoneexit "$exit_code"# expected output:# INSIDE tasks/wallpaper-derivative-check.md# OUTSIDE tasks/theme-diff-against-production.md → url
The non-zero exit is deliberate, because I want this check sitting in front of the unattended pipeline. "A task in this batch reaches outside" is not an error — it is a branch condition. But if I let the branch resolve silently, I will lose another three days, so it stops and asks me to look.
The routing that came out of it: rebuilding derivative images for a wallpaper app and checking them for duplicates stays entirely inside the repository, so it is inside. Touching the theme of a client WordPress site involves fetching the live page to diff against, so it is outside. The three-day stall was, of course, the second one.
Question 2: Will I notice when it stalls?
Outside work can still run unattended if you pre-grant access. The real question is whether you will notice when a missing grant or an expired credential stops it.
Since 1.1.28, fatal errors are printed to stderr with a stable error: marker, which is usable as a signal. But the same release changed --print-timeout expiry to return partial output and exit successfully with a warning, rather than raising a failure. Look only at the exit code and a timeout sails through as a success.
So I check three things instead of one.
#!/usr/bin/env bash# run-unattended.sh — judge an unattended run on three signals, not just the exit codeset -uo pipefailTASK="$1" # e.g. tasks/wallpaper-derivative-check.mdEXPECT="$2" # the artifact this run is supposed to produceLOG="$(mktemp)"agy -p "$(cat "$TASK")" --print-timeout 900 > "${LOG}.out" 2> "${LOG}.err"code=$?# (1) exit code — since 1.1.28 a timeout returns 0# (2) the error: marker on stderr, stabilised in 1.1.28# (3) whether the expected artifact exists — the only signal an approval stall tripsmarker=$(grep -c '^error:' "${LOG}.err" || true)produced=0; [ -s "$EXPECT" ] && produced=1if [ "$code" -ne 0 ] || [ "$marker" -gt 0 ] || [ "$produced" -eq 0 ]; then echo "FAILED task=${TASK} code=${code} marker=${marker} produced=${produced}" echo "--- stderr (tail) ---"; tail -20 "${LOG}.err" exit 1fiecho "OK task=${TASK}"
Some jobs resist naming a single artifact. In that case I have the run emit a one-line summary at the end, and check for that line instead.
Signal three did the real work. A run that stalls on approval reports nothing unusual in the exit code and nothing on the error: marker. The absence of the artifact is the only evidence you get. So every unattended job in my setup now carries a declaration of what it was supposed to produce.
The third question is how expensive it is to undo a bad run.
Derivative images can be regenerated; a failure costs compute time. A client site's theme is a different matter — the live page is what their readers see, and undoing it spends their time as well as mine.
On a site already in production, placing a guard up front is cheaper than cleaning up afterwards.
High-redo-cost work goes on the approval side even when the procedure is trivial. This is the one place I refuse to optimise for throughput. When agents touch more than one repository, there is also the risk of getting the working root itself wrong, so I pair this with the entry check from a guard for the working root before you let agents touch multiple repositories.
On one page, the three questions look like this.
Question
If the answer is yes
Where it goes
Does it reach outside the workspace?
Pre-grant access, or move it to the interactive side
Claude Code (sit-beside side)
Will I notice when it stalls?
Add the artifact check and send it unattended
Antigravity CLI (unattended side)
Is the redo cost high?
Keep an approval step even for simple work
Claude Code (sit-beside side)
Freezing the three questions into one dispatcher
A rule that lives in my head drifts within a week, so I moved the decision into a script.
#!/usr/bin/env bash# dispatch.sh — decide where each task goes, then run it# tasks.tsv: <task_file>\t<expected_artifact>\t<redo_cost:low|high>set -uo pipefailwhile IFS=$'\t' read -r task expect cost; do [ -z "${task:-}" ] && continue case "$task" in \#*) continue ;; esac outside=0 ./scan-external-reach.sh "$task" > /dev/null 2>&1 || outside=1 if [ "$cost" = "high" ]; then route="interactive" # question 3 wins first elif [ "$outside" -eq 1 ] && [ ! -f ".agy/pre-granted-urls" ]; then route="interactive" # question 1: outside reach with no pre-grant else route="unattended" # question 2's three-signal check applies fi case "$route" in unattended) ./run-unattended.sh "$task" "$expect" ;; interactive) echo "QUEUED(interactive) ${task}" >> queue/interactive.txt ;; esacdone < tasks.tsv# expected output:# OK task=tasks/wallpaper-derivative-check.md# QUEUED(interactive) tasks/theme-diff-against-production.md
The order matters. Redo cost is evaluated first because some work belongs in my hands even when it never leaves the workspace. Reverse the order and every simple-looking job drains into the unattended side. The three questions are not parallel: cost, then boundary, then observability.
The .agy/pre-granted-urls check exists because I keep pre-granted access under version control in the repository. A task that reaches outward while that file is absent is precisely the profile of a job that will wait on an approval prompt, so it goes to the interactive queue before it ever runs.
Two weeks in: what changed, and what did not
What changed is that silent stalls stopped happening. When something needs approval it lands in the interactive queue, and reading the queue first thing in the morning is enough. I have not stared at three days of empty logs since.
What did not change is that the judgement-heavy work still ends up in front of me, whichever tool I assign it to. Using two tools does not shrink the part where I am the one who decides. Oddly, accepting that made me more willing to hand the routing itself to a script.
There is one exception I kept resident. The monitoring job that should keep running while my machine sleeps is registered as a service with remote-control (the subcommands arrived in 1.2.0 on September 10, 2026). I wrote about how I think about daemon survival conditions and revocation paths in deciding whether Remote Control belongs on your host machine.
One thing to check tomorrow
Pick one job you run unattended and run it by hand, with exactly the same arguments, under agy -p. If a line asks for approval, that job is waiting at that same line every single night.
I have made this the morning ritual after every version bump. The stopping point will keep moving with each release, and I would rather find it at my own terminal than three days later.
Thank you for reading this far. If it saves even one person the three days I lost, that is enough.
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.