I had handed an image-cleanup command to the agent and stepped away. When I came back, the terminal panel still said Running, and the output hadn't grown by a single line.
I assumed it was just slow and waited about ten minutes. When that didn't help, I doubled the time limit I'd given the agent. It didn't help either: the command simply took longer to not finish.
Looking back, I had been treating "slow" and "the finish signal never arrived" as the same thing. Below I split the second one into three shapes and give the numbers I measured for each.
What I'd say first: change what you suspect
When a command stays on Running, suspect not how heavy the work is but whether the command has any way left to say it's done. In my case the blocked paths came down to three.
| Shape | What is happening | What gives it away |
|---|---|---|
| Waiting on stdin | EOF never arrives, so the command keeps reading | A bare cat, or any read with no file given |
| A lingering child | The parent exited, but a child still holds the output pipe | A trailing &, starting a dev server or a watcher |
| Waiting on a prompt or editor | It stopped at a screen meant for a person | git commit without -m, installers without -y |
One caveat: these numbers come from a plain Linux shell (Python 3.13, git 2.43), not from Antigravity's terminal itself. I can't promise the agent's terminal behaves identically, so I'd rather you confirm in your own setup.
Shape 1: waiting on stdin
A command that reads input won't finish until the input closes. I gave it an input that stays open and cut it off at 3 seconds.
# Make an input that never reaches EOF
timeout 3 sh -c 'cat' < <(sleep 100)
echo "exit=$?" # -> 124 (cut off at 3.0 s: it would not end by itself)
# Close the input up front
timeout 3 sh -c 'cat' < /dev/null
echo "exit=$?" # -> 0 (finished in 0.007 s)python3 -c 'import sys; print(len(sys.stdin.read()))' behaved the same: cut off at 3.0 seconds on an open input, and an immediate 0 with /dev/null.
Here is why I write it this way. < /dev/null tells the command up front that no input is coming, so the reader gets EOF and can end on its own.
Shape 2: a lingering child
This is the one I fell into first. The parent shell is already done, yet whatever is collecting the output keeps waiting.
# Leave a sleep in the background and collect output with cat
timeout 40 sh -c 'sleep 30 & echo started' | cat
# -> "started" appears instantly, but the whole command returns after 30.0 s
# Detach the child's output and input
timeout 40 sh -c 'sleep 30 >/dev/null 2>&1 </dev/null & echo started' | cat
# -> returns in 0.004 sSame &, yet 30.0 seconds versus 0.004. The only difference is whether the child keeps holding the parent's output pipe. If you ask an agent to "start the dev server and move on," this is exactly what you get.
For start-only commands, I always push the output to a file.
# Start the server in the background and let the terminal move on
nohup npm run dev > dev.log 2>&1 < /dev/null &
echo "pid=$!"Shape 3: waiting on a prompt or editor
The third stops at a screen that expects a human. git commit without -m opens an editor and waits. When I forced GIT_EDITOR=true, it didn't wait; it printed Aborting commit due to empty commit message. and returned exit code 1. Instead of hanging, it became a visible failure.
So the fix here isn't to wait out the stopped screen. It's to write the command so the screen never appears.
- Always pass
-mto commits. - Pass
-yor--yesto installers. - If you can't avoid a dialog, don't hand that step to the agent; run it yourself.
Telling the shapes apart
You can tell which of the three it is by peeking from another terminal while the command is stuck. On Linux:
# Find the stuck command's PID, then look at its state and its input
ps -o pid,stat,wchan:14,cmd -p <PID>
ls -l /proc/<PID>/fd/0For the cat above, wchan read anon_pipe_read and stdin was pipe:[2107], meaning it was still reading from a pipe. With /dev/null it had already finished before I could look. (On macOS, lsof -p <PID> gets you close.)
For a lingering child, ps -o pid,ppid,stat,cmd -C sleep showed the sleep 30 with its parent reassigned to 1. The parent was gone and only the child was alive, which you can confirm by eye.
Where it breaks, and the line I draw
These fixes have limits.
< /dev/null breaks commands whose whole purpose is to read input, such as psql < query.sql or docker run -i. And exit code 124 from timeout means "timed out," not "failed," so you can't tell how far the work got. A cutoff narrows down the cause; it doesn't solve anything.
Looking back, doubling the wait first may have been my way of avoiding the effort of looking at the cause. These days I follow this order: before extending the wait, I build a path for the finish signal to arrive. I extend only after confirming the command really is slow.
I put these three lines in the instructions I give the agent.
- For commands that read input, add < /dev/null and wrap them in a 60-second timeout
- Detach background processes with nohup and redirect their output to a file
- Use -m for git commit and -y for installers, so no interactive screen appearsOne next step
If a command is stuck on Running right now, run timeout 20 <the same command> < /dev/null first. If it returns within 20 seconds it was waiting on input; if not, suspect a child process or a prompt next. For the other way work stalls, see Antigravity Keeps Asking Permission for the Same Command? Fix the Approval Dialog Loop, and for telling a timeout apart from a success, see Antigravity CLI 1.1.28 turns a print-timeout overrun into a successful exit — rechecking unattended runs.
As an indie developer who hands routine commands to an agent, this is a check I try not to skip, even on a rushed day.