SikiT

Field notes on running AI agents without a human in the loop.

Field notes on unattended AI agents.

5–7 minutes

Exit code 0 from an agent that did nothing

Three permission modes, one task, identical exit status. Two of the three ended the turn after stating intent and never ran anything, and the calling script had no way to tell.

A table of three permission modes run against the same task. Two report that no command ran; all three exit with status zero.

Sources are linked in this article. Found an error? Report a correction.

I gave the same one-line task to an agent three times, changing only the permission mode. One run did the work. Two ended after a sentence of intent and did nothing. All three exited 0. If a script upstream is branching on the exit code, those three runs are indistinguishable, and two of them are lies by omission.

On this page

Confirm the symptom

Three run transcripts. The first two end after a sentence of intent with exit status zero. The third reports the command output 42 and also exits zero.
The three runs, unedited. Only the permission mode differed.

The task was chosen to leave an unmistakable trace in the reply:

Run this shell command and report its exact output:

python3 -c "print(6*7)"
Report the number you saw, then state whether the command ran.

If the command runs, the number 42 appears. If it does not, no plausible completion is possible, because the agent has nothing to report.

Three runs, one variable:

--permission-mode dontAsk

  "I'll run that Python one-liner now and report the exact output."
  exit=0
--permission-mode default
  "I'll run that Python command now and report the exact output."
  exit=0
--permission-mode bypassPermissions
  "I'll run that Python command now and report the exact output.**42**
   The command ran. Exact output:
   42"
  exit=0

The first two stop mid-turn. There is no error, no diagnostic on standard error, and no non-zero status. The process ended normally after producing a sentence about what it was about to do.

Safest checks before you trust any agent status

The instinct is to check the exit code, then read the reply, then move on. Both of those steps mislead here, in different ways.

  1. The exit code describes the process, not the task. The CLI started, handled its input, and shut down cleanly. That is what 0 means. Whether the work happened is a different question that the exit code was never carrying.
  2. The reply reads as progress. "I’ll run that command now" is the opening of a successful run and the entirety of a blocked one. Truncation looks like a preamble.

The only reliable check is the trace the task was supposed to leave. Choose tasks that leave one, and assert on it. A task whose completion you cannot verify from outside the agent is a task you cannot delegate unattended.

Find the cause: the gate is silent by design

The two failing modes are the ones that gate an action behind approval. default asks. dontAsk declines to ask, which in a non-interactive session means the action does not proceed.

Neither is a bug on its own. A permission gate that refuses an unapproved action is doing its job. The problem is the reporting contract around it: the refusal is not surfaced as a failure to the process boundary. Nothing is written to standard error. The status stays 0.

That combination produces the specific shape worth naming: a run that is correctly restricted and incorrectly reported. The restriction worked. The report did not mention it.

It is also why this failure is hard to attribute. In a non-interactive context the prompt for approval has nowhere to appear, so the mode that seems most automation-friendly, the one that promises not to interrupt you with questions, is the one that quietly does the least.

Fix the cause: assert on the trace, not the status

Three changes, from cheapest to most durable.

Put a verifiable artifact in the task. Ask for something you can check without the agent’s cooperation: a file with known contents, a line appended to a log, a value written where the caller can read it. Then have the caller check for it. exit 0 plus the artifact means done. exit 0 without the artifact means the run was truncated.

Make the caller fail closed. The wrapper should treat a missing artifact as a failure regardless of status:

"$AGENT" ... ; rc=$?

[ "$rc" -eq 0 ] || exit "$rc"
[ -f "$EXPECTED" ] || { echo "agent exited 0 but produced no result" >&2; exit 70; }

The distinct exit code matters. It separates "the tool broke" from "the tool ran and the work did not happen," and those need different responses.

Pin the permission mode deliberately, and record which one ran. If a task needs to execute commands, it needs a mode that permits execution, and the mode should be part of the run record rather than a default someone can change later without noticing. A run log that does not say which permission mode was in effect cannot explain its own empty result.

Check again: run the same task twice

Verify the wrapper, not just the agent. Run the identical task under a mode that permits the action and under one that does not, and confirm the caller reports them differently.

If both runs report success, the fix is not in place. If the permitted run reports failure, the artifact check is looking for the wrong thing.

This is a two-minute test that stays valid after upgrades, which matters because permission-mode defaults and names are exactly the kind of surface that changes between versions.

Limits and evidence

One CLI, one version, on macOS, tested 2026-08-28 with three runs that differ in one flag. The behavior of default in an interactive terminal is different by design: there the approval prompt appears and a person answers it. Everything above concerns non-interactive use, where there is no one to ask.

I did not test whether other permission modes in the same tool behave the same way, and I did not test other agent CLIs. What generalizes is not the flag names. It is that a permission refusal and a completed task can share an exit status, and that the reply text does not distinguish them.

I also cannot say from these runs whether the agent knew it had been blocked. The transcripts end after the intent sentence with no acknowledgment either way, which is itself the reason the caller has to do the checking.

Sources

The three transcripts and their exit statuses are reproduced above verbatim, as direct checks against the installed binary. The CLI’s help output lists the permission modes as bare values with no description of what each does when no one is available to approve an action.

POSIX defines the distinction the exit status cannot carry here. A caller can tell a normal exit from a signal kill, because those are separate macros over the same status word: WIFSIGNALED "evaluates to a non-zero value if status was returned for a child process that terminated due to the receipt of a signal that was not caught." There is no equivalent for "the process ran fine and declined to do the work," which is why that case has to be detected by checking for the artifact.

Related articles

Stay in the loop

Get new practical AI and technology articles in your inbox. Unsubscribe anytime.

Comments

Questions, corrections, and useful counterpoints are welcome. Keep comments specific and on topic.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Thanks for commenting

Get new practical AI and technology articles in your inbox. Unsubscribe anytime.

Return to the comments