What Actually Runs When You Walk Away: KiroCrew’s Autonomy Modes, Compared


Once a KiroCrew session is running, there are four ways work can proceed without you driving every turn: Autopilot, the Task Runner, Monitor, and /goal. The docs present them as separate features, and I could not tell which one I was supposed to reach for — so I spent an afternoon working through them question by question, reading the installed source as I went rather than trusting the feature list.

They are not four peers. Two of them are the same engine wearing different hats. One of them is not actually about stepping back at all. And the gotcha that broke my first test was an approval gate that had nothing to do with any of the four.

First, a distinction the feature list blurs

Before comparing them it is worth separating two questions that KiroCrew’s documentation tends to answer in the same breath: what starts the work, and how the work proceeds once started.

CategoryAnswersMembers
TriggersWhat starts the work?Scheduled (cron), reactive (webhooks, messaging events), interactive (you typing)
Execution shapesHow does it proceed?Autopilot, Task Runner, Monitor, /goal, subagents

Scheduled jobs are a trigger, not a shape. A cron job can itself run an Autopilot plan, fan out to subagents, or just do one thing and stop. It is orthogonal to the four modes below, which is why I am not comparing it against them.

It is also worth saying that cron is the purest automation of anything here, because a script or command cron runs with no model call at all — deterministic code on a schedule, zero tokens. Everything in the right-hand column is “an agent does the work.” A script cron is “code does the work, on time.” If a job is a fixed sequence with a mechanical decision at the end, that is the tool, and reaching for an agent instead just adds non-determinism and cost. This post is about the four cases where you genuinely do want an agent’s judgement in the loop.

The four, arranged by how much you supervise

ModeShapeWhere you are
AutopilotPlan → you approve → staged executionPresent, gating each stage
Task RunnerExecutes a spec file of steps you wrote in advanceEither — depends how you launch it
Monitor (babysit)Poll something until a condition is metAway
/goalIterate toward an outcome until evidence says doneAway

Monitor and /goal are the two that share an engine. That engine is AutoNudge, and it is worth understanding first because everything built on it inherits its behaviour.

AutoNudge: the engine, not a mode

AutoNudge is a Gateway service, not something you invoke. It does one thing: when a turn completes and the session then sits idle for idle_secs, it injects a configured message as the next turn into the same session — same context, same tools, same conversation. The nudge appears tagged [auto-nudge cycle N] so you can tell it from something you typed.

  • Idle-based, not periodic — for dashboard sessions. The timer measures the gap after your turn ends, so a 300-second interval on a five-minute check wakes you roughly every ten minutes, not every five. Messaging-channel sessions are the exception: they have no dashboard turn-lifecycle hooks, so those loops run on a fixed interval that re-arms right after each fire.
  • Survives restarts. State persists to autonudge.json; active loops are reloaded and their timers re-armed when the Gateway comes back.
  • Bounded three ways — a max_cycles cap, a per-loop STOP sentinel file, and exponential backoff on failed fires (15s base escalating to a 5-minute ceiling, so a wedged slot degrades to a slow poll instead of hammering).
  • Per-slot and opt-in. The service being enabled does not mean anything is looping. One loop per session, created only when something explicitly arms it.

IMPORTANT: Monitor and /goal are not separate engines. Both call the same AutoNudgeService.add() with an interval, a cycle cap, a sentinel path, and a message. The only difference is what that injected message instructs. Monitor says “check this and report real signals.” /goal says “do one atomic step toward the objective.”

That is a genuinely good design — one bounded, restart-safe loop primitive, with the behaviour expressed as instructions rather than as more code. But it means the two features share every limitation, and learning the engine tells you more than reading either feature page.

The /goal name collision

Here is where reading the source paid for itself. The documentation described /goal as a loop with a dedicated completion tool, a default of five iterations, and exponential-backoff dispatch retry. The installed dashboard code says something different: /goal is a thin wrapper over AutoNudge — the handler docstring literally calls it a “v0 self-verdict loop” — with a default cap of 50.

What I could verify is that the dashboard handler builds a plain-English nudge and arms an AutoNudge loop; it never forwards the string to the agent process. What I could not verify is the other half: kiro-cli ships as a compiled binary, so the /goal the documentation describes is something I am inferring from the docs rather than reading. The most likely explanation is two independent implementations sharing a name — but treat that as inference, not a finding.

The nudge /goal actually injects tells the agent, in order: stop if the sentinel file exists; stop if the goal is met by concrete evidence (a passing test, a built file, command output — not a guess) and cite it; otherwise do one atomic step of at most five tool calls, and make the deliverable durable before claiming progress. Guardrails in the same text: never push to git, never read credential files, and on a hard blocker say so once and stop.

IMPORTANT: that completion contract lives entirely in the injected prompt text, not in a code-enforced gate. It is a well-written instruction, and for a v0 that is a reasonable place for it. It is still a soft control: the same agent doing the work decides whether the work is done.

Autopilot and /goal decompose at opposite ends

Both drive multi-step work, so I assumed they overlapped. The deepest difference is when the work gets broken down.

Autopilot decomposes up front. It is a per-session mode (internally the orchestrator slot mode) that writes all the stages before any work starts, presents them, and ends the turn. You get three controls: Go (next stage only, then pause again), Go All (all remaining stages without pausing, halting on failure or escalation), or Cancel. Stages are sequential, tasks within a stage can fan out in parallel, and the last stage must be verification.

/goal decomposes lazily. No plan, no approval — one atomic step per idle cycle, route emerging as it goes.

Autopilot/goal
PlanAll stages, reviewable up frontNone — emergent
ApprovalYours, per stage or per runNone
TriggerYour clickIdle timer
RetryUp to 3 rounds within a stage, then checkpoint and ask youRetries differently each cycle up to the cap
Built forYou presentYou absent

I initially had Autopilot down as fire-once-and-stop, which is wrong. A stage can take multiple rounds — spawn a batch of sub-agents, wait, then spawn more if the stage goal is not met — with a documented ceiling of three rounds per stage, after which it checkpoints what it has and asks you. So both modes iterate; the difference is that Autopilot’s iteration is bounded per stage and escalates to a human, while /goal’s runs to its cycle budget and escalates to nobody.

That is the useful selection rule. For “make all the tests pass,” where the path is trial and error and the outcome is mechanically checkable, /goal fits. For “migrate this module,” where the approach is the risky part and a wrong plan wastes a lot of work, Autopilot fits.

Where the Task Runner sits

The Task Runner is the third shape: it executes a markdown spec file of steps you wrote in advance, with live per-step progress and multi-turn refinement afterwards. Not interactive planning like Autopilot, not emergent like /goal — just “here are the steps, run them.”

IMPORTANT: its tool approval depends on how you launch it. From the dashboard, chat, or Slack, non-allowlisted tools prompt interactively. From the standalone CLI (kirocrew run TASK.md) there is no interactive channel, so it is deny-by-default — a tool runs only if it matches hooks.auto_approve_tools, otherwise it is rejected and logged as headless_no_authorization.

That is the first hint of the thing that actually tripped me up.

How you actually start each one

This is the part I found least obvious, and it explains a lot of the confusion: the four are not invoked the same way at all. Only one of them is a command you type.

ModeHow you start it
/goalA slash command you type: /goal <objective>, optionally --max N. /goal clear stops it, /goal status checks it.
AutopilotA per-session toggle in the dashboard. You can also just ask — “autopilot this”, “create a plan” — which overrides the agent’s own judgement about whether the task warrants planning and forces a plan.
Task RunnerPoint it at a spec file: run <path> in chat or Slack, the Tasks page, the task_run tool, or kirocrew run TASK.md from a terminal.
MonitorNo command exists. You ask in plain language — “keep checking…”, “babysit this…”, “let me know when…” — and the agent calls monitor_start on your behalf.

That last row is worth dwelling on. I went looking for a /monitor command and there isn’t one — the dashboard’s slash-command list has /goal and no monitor equivalent. So two features built on the same engine have completely different front doors: one is a typed command, the other is something you request conversationally and the agent wires up.

That asymmetry is probably why they read as unrelated features. /goal feels like a product because it has a command. Monitor feels like a capability because it does not.

And how you stop them

Both loop modes stop the same four ways, because both are AutoNudge underneath: ask the agent (it calls autonudge_stop), use the dashboard’s loop popover, DELETE /api/autonudge/{loop_id} over the REST API, or create the loop’s STOP sentinel file — which halts it on the next cycle. /goal clear is just a convenience wrapper on the same removal.

IMPORTANT: there is one loop per session. Arming a monitor replaces an existing /goal loop on that slot, and vice versa — they are competing for the same single slot on the same engine. If you want both a standing goal and a watch running, they need to be in different sessions.

Stopping deliberately matters more than it sounds, because a loop survives a Gateway restart — its state is reloaded and the timer re-armed. Walking away from a live loop does not end it.

The gotcha that actually bit me

I armed a /goal loop as a test. It stopped almost immediately and asked me to approve running ls — which defeats the entire point of a loop designed to run while I am away.

My first assumption was a misconfiguration, because my config has:

"agent": {
"approval_mode": "auto"
}

If tool calls auto-approve, why the prompt? My first answer was that approval_mode only governs tools routed through KiroCrew’s MCP gateway, so an agent-native tool like execute_bash falls outside it. That was also wrong, and finding out why produced the actual lesson of this whole exercise.

Grepping for every reader of agent.approval_mode — rather than stopping at the first one — turns it up well outside the MCP path. Three readers I could verify at a real call site:

  • Messaging-channel gateways (Telegram, Discord, Slack, Teams, Webex, WeCom, Weixin) — a shared _resolve_approval_mode() reads orch._cfg.agent.approval_mode and feeds it into the channel’s approval ladder
  • Subagentsself._global_approval_mode = KiroCrewConfig.load().agent.approval_mode
  • The MCP gateway overlay — the gateway passes approval_mode=self._cfg.agent.approval_mode into the agent-spec rewriter, which forwards it to the MCP stub and folds it into the stub’s pool key alongside the sandbox mode

The messaging resolver is unambiguous about its breadth: YOLO auto-approves, otherwise a CLI override or the configured agent.approval_mode decides, collapsing anything that is not auto to interactive. That is a general tool-approval default, not an MCP detail.

A comment elsewhere in the codebase makes the consequence explicit, and it is the sharpest thing I found all day — the default fails open:

omitting --approval does not mean “interactive”. The gateway leaves approval_mode unset, and slack/events.py falls through to cfg.agent.approval_mode, which config/loader.py defaults to “auto” — auto-approve every tool. Dropping would therefore be the LEAST restrictive outcome. Pin interactive explicitly instead.

IMPORTANT: the decisive fact turned out to be an absence. The dashboard chat path never reads cfg.agent.approval_mode at all. Its approval decision is built from per-slot trust, YOLO state, and hooks — and it then derives the session’s approval policy from those:

# Propagate trust/YOLO to session so subagents inherit auto-approve.
if slot._trust or state.is_yolo_active():
state.sessions.set_approval_policy(session_key, "auto")
else:
state.sessions.set_approval_policy(session_key, "")

So approvals are governed per surface, not per tool class:

SurfaceApproval decided by
Dashboard chatper-slot trust, YOLO, hooks — not agent.approval_mode
Messaging channelsagent.approval_mode (or a CLI override / YOLO) via the turn-driver ladder
Subagentsthe spawn’s own approval_mode, defaulting from the global config
Cron jobsthe job’s own approval_mode field
MCP tool routingagent.approval_mode forwarded to the stub

Which means my ls prompt is explained by the surface I was on ignoring that setting entirely — not by the tool being native. Separately, execute_bash does appear in my agent’s tools (available) but not in allowedTools (auto-approved), along with fs_read, fs_write, grep and glob. Both facts are true; only the second one is the part I originally got right, and I had attached it to the wrong cause.

Two honest limits. I have not fully disentangled, for a dashboard turn, how much of the outcome is the trust check versus the agent-level allowlist — I can show that the dashboard decides from trust and that the tool is not allowlisted, but not which one is doing the refusing on any given call. And allowedTools is enforced inside the agent binary, so how its entries are matched comes from documentation rather than code I read.

This matters more than a stray click, because approval prompts auto-decline after ten minutes. An unattended loop needing a non-allowlisted tool will fire a cycle, wait, get declined, and burn the cycle having achieved nothing — repeating until it exhausts its budget. So the fix is not approval_mode. It is one of:

  • Session-scoped trust — the approval path has trust tiers above plain interactive prompting, so trust granted for a session stops the prompting for that session without changing anything on disk. Suitable for a one-off unattended run.
  • A dedicated agent whose allowedTools auto-approves only the tools that job needs. More setup, but it is the least-privilege answer and it survives across runs.

Widening allowedTools on your main agent also works, and it is the option I would think hardest about: it auto-approves execute_bash for every conversation from then on. That is a permanent change to what runs without asking, in exchange for fixing one afternoon’s convenience problem.

Takeaways

  • Two of the four share an engine. AutoNudge is infrastructure, not a mode — Monitor and /goal are different instructions injected into the same bounded loop. Learn the engine’s limits (cap, sentinel, backoff, idle timing) and you understand both features.
  • Choose on supervision, not capability. Autopilot when the approach needs reviewing. /goal when the outcome is verifiable but the path is trial and error. Task Runner when you already know the steps.
  • Both iterate — they escalate differently. Autopilot retries up to three rounds per stage then asks you; /goal retries to its cycle budget and asks nobody. That, not the presence of retry, is the real difference.
  • They are not invoked alike. Only /goal is a typed command. Autopilot is a session toggle, the Task Runner takes a spec file, and Monitor has no command at all — you ask for it and the agent arms it. Two features on one engine, with two completely different front doors.
  • One loop per session. Monitor and /goal compete for the same slot, so arming one replaces the other. And a loop survives a Gateway restart — stop it deliberately.
  • Approvals are governed per surface, not per tool class — and the surface you are on may ignore the setting you just changed. The dashboard does not read agent.approval_mode; messaging channels, subagents, cron jobs and MCP routing each resolve it differently. Check the surface before trusting a loop to run unattended, because getting this wrong produces a loop that silently burns its budget on declined prompts.
  • A self-verdict is a soft control. /goal’s evidence requirement is prompt text, not enforced code — fine for a v0, as long as you are not treating it as a guarantee.

The general lesson is not specific to KiroCrew: when several features look like they solve the same problem, the naming rarely tells you the difference, and the documentation sometimes describes a different build than the one in front of you. I got three things wrong while working through this — I had AutoNudge filed as a feature rather than the plumbing, I had Autopilot down as fire-once when it has a three-round budget per stage, and I twice explained approval_mode wrongly (first too broadly, then too narrowly) before finding that the surface I was testing on does not read it at all.

The approval_mode error is the instructive one, because it kept recurring for the same reason: I generalised from the first call site I found instead of enumerating every reader of the key. Doing that once gives you a plausible answer. Doing it properly reveals that the decisive fact can be an absence — that your surface never consults the setting at all — which no single call site can show you.