Skip to content

AFK agents

You are watching an agent do work you already knew how to describe, typing corrections you could have written down before it started. That is the tell: the task is specified, your attention is adding nothing but latency, and you are paying for it in wall-clock hours. This play is about not being there. The difficulty is that the good ending and the bad ending start from the same keystroke.

Use whenA bug with a reproduction, a refactor with clear edges - anything you could describe completely up front
You’ll haveA PR on a fresh branch, sized to one review sitting, plus a spec and its running log that get better every run
CostsOne watched run to certify the setup. After that, zero attention between kickoff and the notification
NeedsSomewhere isolated for it to run - sandbox, container, or cloud surface - and a credential you can scope and revoke
Skip ifYou can’t write the acceptance criteria yet, or two people would be editing the spec while the run is live

An unattended run has no steering wheel, and no memory beyond what’s written down. Every correction you’d make mid-session - “no, not that file” - has to be front-loaded into the spec, a rules file, or permissions that make the wrong move impossible rather than merely discouraged.

It self-corrects onIt cannot see
A crashed buildAn ambiguous requirement
An empty result setAn assumption that’s wrong but still runs green
A red suite it reruns until greenA constraint compaction paraphrased into something vaguer

Newer models do the left column impressively well. A run that notices its first attempt worked on missing data and relaunches a fixed version is the loop working as designed, not a counterexample. On the right column it can’t ask, so it guesses and keeps going, fluently, in the same tone either way. The gap this discipline closes isn’t self-correction. It’s judgment on the problems that never announce themselves.

So every gate below has one shape: route the agent’s claim through a check that can say no - a file, a diff, a suite it can’t edit - never its own report. Here is what the gates are for:

If it goes wrongWhat it costsWhich wall stops it
A force-pushed branchHistory other people were working onA deny rule on force-push, plus output landing as a PR instead of a push
A .env in a commit messageA key rotation, and an audit of everywhere that key reachedA Read(.env) deny rule, plus a secrets scan on the diff at review time
An agent looping all night on a dependency it broke at 9 p.m.The night, and nothing to show at breakfastThe stop-gate’s iteration cap
A bill instead of a PRCurrency, quietly, while the run “worked” the whole timeThe cost wall: a per-run ceiling and a turn cap

Autonomy is a dial you turn up run by run, not a switch you flip on day one.

Each rung certifies something the next one spends. Skipping one is the whole failure mode in miniature: you get the confidence of a run you watched, applied to a run you didn’t.

RungWhat you doWhat it provesWhat skipping it costs
0 - attendedType every command yourselfNothing. It’s the baseline you’re leaving, not a rung to linger on-
1 - watchedKick off the exact run you intend to background, stay attached, read the live logThat the run’s middle is sane, not just its summaryThe failures that never reach the summary. See below
2 - corrections foldedWrite every correction you’d have typed into task.md or decisions.md, re-run clean, repeat until zero typed correctionsThe spec is complete enough to survive your absenceA correction you’re about to type again. It belongs in the spec, and unattended runs never get it at all
3 - identical invocationHand the unattended run the same file that just passed Rung 2, one flag differentThat what you certified is what runsThe supervised pass certified a different, better-behaved agent than the one about to run without you
4 - wider scopeA bigger task, a longer leash, or a schedule or event trigger instead of a manual kickoffNothing new. It spends the trust the rungs below earnedWidening before Rung 3 is clean is how a fixed bug becomes an overnight incident

Rung 1 is the rung people skip, and it is where the invisible failures live. One practitioner watching a background run’s live log saw the agent hit a missing dev-server dependency, install it, fail, reinstall, and circle that drain for a long stretch - invisible in the final “done” summary, which read exactly like every other final summary. You only catch that kind of thing by watching, not by reading the report afterward.

Reading a live log is always allowed. The line you don’t cross is typing into it: a mid-run correction is a spec line that doesn’t exist yet, and folding it into the file is Rung 2’s entire job.

Rung 3 is one flag, and nothing else:

supervised: agent run --prompt task.md
unattended: agent run --prompt task.md --headless

If the two invocations read different files, the supervised pass certified something else. The same logic applies to the model, not just the flag: certify and run on the same tier, because a Rung-2 pass on your strongest model says nothing about how a cheaper default behaves alone at 3 a.m.

Some tasks never earn a rung. If you can’t write the acceptance criteria yet - find out why p99 regressed, see if we can drop Redis - freezing an open question into a task file just automates the guessing. Keep exploratory and taste-heavy work supervised, where your mid-course corrections are the value. And skip the play entirely when two people would be editing the same task.md while a run is live: that’s a coordination problem no file format fixes.

Size what’s left from both ends: small enough on the way out that the PR fits one review sitting, small enough on the way in that the task finishes inside one context window. Past that, use the checklist shape from Scaling up, not a longer leash.

  1. Write the task as a spec, completely - then give it two companion files. task.md is the frozen intent: context, why, acceptance criteria, edge cases, test plan, and an explicit allow-list of what it may touch - everything you’d otherwise clarify mid-session, because there is no mid-session:

    # task.md - fix-flaky-checkout-tests
    Fix the flaky checkout tests. Only touch files under tests/ and
    src/**/clock.ts - never change test intent, only nondeterminism
    (ordering, time, unseeded random, shared state). Never edit
    migrations/ or CI config.
    Done when: `npm test` passes 3x in a row, with the same test files,
    byte-for-byte, as when you started.

    Next to it, two files the run itself keeps current - an append-only pair, never overwritten, so every entry is a receipt rather than a rewrite of history:

    progress.md
    [14:02] Started. 3 failing tests identified: checkout.spec.ts:41,
    checkout.spec.ts:88, cart-timer.spec.ts:12.
    [14:19] :41 was ordering-dependent on a shared `cart` fixture - moved
    to per-test setup. Suite green on isolated run, red in full run.
    [14:41] :88 and :12 both race against `Date.now()` - installing a
    fake clock (src/lib/clock.ts) instead of touching assertions.
    [15:03] Full suite green x1. Re-running x2 more per done-condition.
    decisions.md
    [14:19] Chose per-test fixture setup over a shared beforeEach reset -
    reset order still wasn't guaranteed across parallel workers.
    [14:41] Introduced src/lib/clock.ts as the one seam for time in
    tests, rather than patching Date.now() at each call site - matches
    the "only touch clock.ts" boundary in task.md, and the next flaky-
    time test gets a documented seam to use instead of another patch.

    Three files instead of one, because compaction reliably keeps the goal, the decisions made, and which files matter - it reliably drops the exact command output and the precise sequence of steps. On a run long enough to compact, or one that dies and restarts from a clean session, the agent re-reads the trio off disk instead of depending on a shrinking conversation to remember what it already tried.

    That is also what makes a stateless retry loop reliable at all: fresh context every cycle, durable state on disk. A GitHub issue works even better than a loose task.md - the PR links back to it, and when it auto-closes on merge, future-you can still see why. For multi-part work, make the spec a checklist where every item carries its own validation step, so a failure at item six can’t be silently built on top of an unverified item three.

    Before you walk away: someone who isn’t you could judge done-versus-not-done from task.md alone, and reconstruct what was tried - and why - from progress.md and decisions.md.

  2. Shrink the blast radius with several thin walls, not one switch. Each one stops something the others can’t, and the third column is the reason you need more than one:

    WallStopsDoesn’t stopWhere it lives
    Permissions allow/denyThe common destructive command, cheaply, by path and command textA dozen spellings of rm -rf, or the same command hidden inside a wrapper script it never readsYour tool’s settings file - full block in the kit
    The sandboxWhatever any spelling can actually reachA wrong-but-plausible change inside the repo it’s allowed to editThe container, VM, or cloud surface the run happens in
    A scoped credentialA leak costing more than one disposable secret: one repo, read plus PR-write, no admin, revoked when the run endsAnything inside the scope you grantedYour git host’s token settings
    A blocking hookWhat path-matching can’t see: a secret-shaped string in a diff, a destructive SQL statementWhatever you didn’t think to write a matcher forPer-tool hook config - see tool notes
    The cost wallThe run that works correctly, expensively, foreverAny damage that isn’t measured in moneyA per-run ceiling and a turn cap - step 3

    One deny rule earns its place ahead of all the others:

    "deny": ["Edit(.claude/settings.json)"]

    It protects the config file that defines these very rules. If the agent can edit the file that grants its own permissions, the permissions aren’t a wall - they’re a note it can amend.

    Before you walk away: no allowed tool call can edit the walls or the file that defines them - confirmed, not assumed.

  3. Set the cost wall, in whatever units your surface bills in. It’s the easiest wall to skip, because a leaked secret feels dangerous in a way a big bill doesn’t - until you’re staring at one:

    How your surface billsWhat the wall isThe flag or check
    Metered spendA hard per-run dollar ceiling, plus a turn cap so a run spending slowly but never finishing still exitsA spend-ceiling flag and an iteration cap - names in tool notes
    Subscription tierA model-tier check before a multi-hour runConfirm which model the background job defaults to. A silent escalation to your priciest tier is the same shape of surprise as an uncapped API key
    A local model, your hardwareWall-clock and the machine. There’s no dollar meter to blowA turn cap or a timeout instead of a budget

    Two walls, not one: a ceiling stops a run that spends fast, an iteration cap stops a run that spends slowly and never finishes. Set both. Every surface has a wall worth setting and only the units change, so the question to ask your tool’s docs is “what halts this run against my will,” not “does it have the flag I read about somewhere.”

  4. Answer three questions out loud. Walk away only on three good answers:

    1. If this run did the single worst thing it could, what could it reach?
    2. What would the worst hour cost, in currency?
    3. Can any allowed tool call edit the walls or the gate's own config?

    Before you walk away: “almost nothing,” “a number I’d shrug at,” and “no.”

FailureTellFix
The cost blowoutAn invoice, not a broken PR. Or, before the fact: no dollar ceiling set and you’re about to background a run anywayThe wall goes on before the first unattended run, not after the first surprising bill
The wall that isn’t a wallA settings or config file the agent’s own allow-list can editThe self-protecting deny rule in step 2, verified by question 3

A run with no dollar ceiling doesn’t fail loudly. It keeps calling the model, correctly, at your most expensive tier, for hours. Practitioners running unattended loops overnight have reported bills running into the thousands of dollars from a single unwatched session - not from a bug, but from the plain absence of a ceiling.

  1. Give the done-condition teeth, behind a stop-gate the agent can’t edit. “Tests pass” is fine until the agent makes it true by deleting the failing assertion, so point the check at something outside its write access: a test file it can’t touch, or a verifier in a separate step. Don’t prompt for persistence (“keep going until it’s really done”) - wire it, on the event that fires whenever the agent tries to stop:

    on Stop:
    run the suite
    if suite fails:
    if iterations < max_iterations:
    iterations += 1
    block, feed the failure output back into context
    else:
    block, message: "iteration cap reached - stopping for a human"
    else:
    allow the stop

    Two parameters are mandatory: the max-iteration cap and the checkable completion condition. Omit either and you’ve built a loop with no exit - it will run all night, politely, expensively, forever. The gate is only a gate if the agent’s own tool calls can’t reach the file that defines it, which is why that deny rule belongs on the permissions list from the start rather than as an afterthought. Per-tool wiring is in tool notes.

    Before you walk away: the loop has an exit the agent can’t edit, and a cap.

  2. Detach, land as a PR, and come back only on notification. The entry point varies - chat UI, CLI flag, an assigned issue, an @mention - but the run leaves your session the moment it starts (headless invocation underneath; closing the laptop doesn’t stop it, because it isn’t running on the laptop). Output lands on a new branch as a PR, never a push to anything protected. Auto-merge is an opt-in escalation, not a default.

    The notification channel is the one genuinely tool-specific thing here - in-app, email, mobile push, Slack - so confirm which one your surface actually uses before you rely on it. Before you walk away: zero attention spent between kickoff and the notification.

FailureTellFix
The run that outlived its instructionsLate-run drift from the spec’s exact wording, with nobody there to noticeSmaller tasks. The spec file re-read from disk beats a constraint remembered from turn one
Blaming the model for the plumbingA weird failure with no coherent story in the logBackground dashboards, mobile surfaces, and repo connectors are young infrastructure. Suspect the connector and retry before you re-spec

Recovery from drift, once you notice: don’t keep arguing with the drifted session. It’s now defending a plan it built on a paraphrase of your original constraint, not the constraint itself. Stop it, re-read task.md and decisions.md yourself to confirm they still say what you meant, and start a fresh run against those exact files. The fix is a new attempt reading the same disk state - that’s the whole reason the trio exists, so restart costs “read three files” instead of “re-explain from scratch.”

A night’s work comes back nine-for-nine. What that “done” has to get past before it means anything is your call:

Nine chores, queued for an agent to run overnight. The terminal replays the night as it happened; the panel is the morning report - the only thing you’ll actually read. Watch what its checkmarks are made of, and when playback pauses, decide what the run has to get past before you believe it.

overnight batch - ledger-app
overnight batch · repo: ledger-app · 9 chores queued · nobody watching
the morning report

    The flaw rate here is theatrical - seven duds in nine - because the point is what each wiring would have caught, not the odds. The checks wear different clothes per tool (a CI step, a test command the loop must pass, a verifier agent), but the question is always the same: what does this run have to get past before “done” reaches you? And the residual is real: no check catches a requirement nobody wrote down.

    1. Review it like a stranger’s PR. The diff is testimony from something that cannot doubt itself: “all tests passing” reads identically whether the tests were fixed or quietly gutted, and the only signal that survived to your review is the one the agent chose to report. Same list, every time:

      • Read the diff, not the summary. The summary is the agent’s own account of its work. The diff is the work.
      • Scan for secrets before anything else. A committed .env, a pasted API key in a comment, a token in a test fixture. Run gitleaks, trufflehog, or your platform’s native scanning against the diff, not the whole repo, so it finishes before your coffee does.
      • Check scope against the spec’s allow-list, not against “does this look reasonable.” A change reaching into a file task.md excluded is a violation even if the change itself is good.
      • Demand suite evidence, not a suite claim. A green CI check, or a pasted run with a test count you’d expect. If the only evidence is the agent’s own prose, the claim is unverified.
      • Only when behavior is meant to be identical (a refactor or migration): run the old and new code paths on the same inputs and diff the outputs.

      That last one earns its place for the change class where a green suite lies most easily. The suite proves the cases you thought to write; a divergence between old and new surfaces the behavior you didn’t. It’s the artifact principle again - the old implementation is a reference the agent can’t edit, so a mismatch is one it can’t explain away. Skip it for a genuine feature change, where old and new should differ.

      Even practitioners who sell the walk-away lifestyle hardest add the same footnote: review and test before you merge. One upgrade is two independent reviewers with different lenses. In one team’s setup a security-focused pass said ship it, while a QA-focused pass on the same change found real problems.

      Before you walk away again: you read the diff, ran a secrets scan, and confirmed the touched-files list against the spec - not just the green check.

    2. Iterate where it landed. A PR comment, another @mention, a resumed session from another device. The original conversation no longer exists, and that’s fine: task.md, progress.md, decisions.md, and the branch are the state. Before you walk away again: follow-up requests reference the PR, not a chat history.

    FailureTellFix
    The collisionMerge conflicts with yourself - you kept editing the same files while the run was outParallelize along ownership lines: one agent on the UI, another on the API, you on neither
    The unreviewable green PRYou scroll the diff instead of reading it. Or a “done” summary arrives with no diff attached at allUpstream, not downstream: scope the spec so the artifact fits one review sitting

    If the collision already happened, don’t hand-merge two divergent implementations of the same function - that produces a third, untested one. Diff each branch against the common ancestor, decide whose version is the real one, and re-apply the other side’s intent - usually a smaller, nameable change - on top of the winner, as a fresh small commit. A rebase that silently auto-resolves both sides is worse than a conflict marker that forces you to look.

    One run, kicked off by hand, on a task you wrote today is the base case. The invariant survives all three growth directions: state on disk, walls the run can’t edit, output as a PR.

    ScaleWhat changesWhat new failure it invites
    The recurring run - nightly triage, weekly dependency bumpVersion task.md and review its diffs like code. Tune the notification to action needed, not run finishedSpec drift: a “small clarification” at week three is an unreviewed behavior change multiplied by every future run. And a per-run ceiling you’d shrug at once is a monthly line item at thirty runs
    The fleet - several agents at oncePartition before kickoff, along ownership lines: one agent per module, one branch each, no two runs writing the same files. Decide merge order up frontThe collision, invited on purpose. “Whoever finishes first merges first” is how two green branches make one red main
    The run too big for one windowDesign for restart, not endurance. The spec becomes a checklist where every item ends in its own commit plus a progress.md entryNothing new, but compaction stops being a risk and becomes a certainty

    A run that dies at item six restarts from a clean session, re-reads the trio, sees five committed items, and resumes at six. The unit of survival is the checkpoint, not the conversation.

    The other half of every checklist item is what counts as its validation, and that is a play of its own: TDD with agents is where the check gets written. An unattended run is the case that most rewards having one, because a test that predates the fix is the only reviewer awake at 3am.

    As of mid-2026 - verify against current docs before relying on exact names, they move:

    ToolAsync surface(s)Kick it off fromWhere output landsHow you’re notified
    Claude CodeClaude Code on the web (cloud) · Routines (scheduled cloud) · Desktop scheduled tasks · session-scoped /loopweb/mobile session, a schedule, /loop, --teleport to reattach a cloud session locallycommits/PRs from cloud sessions; your existing local branch for /loop and Desktop tasksin-app; cloud sessions can be monitored and steered from the Claude mobile app
    CodexCodex cloudChatGPT Codex UI, IDE “cloud delegation,” @codex on a GitHub issue/PR commentopens a GitHub PR, or pull the diff down locally insteadGitHub notification on the issue/PR; Codex UI
    GitHub CopilotCoding agent (ephemeral GitHub Actions environment)assign a GitHub Issue, @copilot in a comment, the Agents panel, VS Codedraft or ready-for-review PR, never a direct commitAgents panel, IDE, GitHub Mobile push; every commit links back to its session log
    CursorCloud Agents (renamed from “Background Agents” - treat the old name as legacy)chat UI, dashboard, or an event trigger (Slack message, new Linear issue, merged GitHub PR, PagerDuty incident)PR on a new agent/<task-slug> branch; your working branch is untouchedemail, desktop notification, Slack if connected - check the default model tier before a long run
    OpenCodeNo first-party cloud/hosted surfaceopencode serve + a remote TUI attach; background subagents alongside the primary sessionyour existing local working treenone - the model is “you’re still attached,” not “walk away”
    PiNone, by explicit design---

    OpenCode’s own docs describe no fresh-clone or cloud-container execution model at the core-product level - that’s a real gap, not an oversight to route around. The closest analog is running opencode serve on a remote box you already control and attaching a TUI to it, which gets you “not on my laptop” without “not my responsibility while it runs.”

    Pi is more explicit still: its own site lists “no background bash” under what it deliberately didn’t build, with the stated alternative “use tmux - full observability, direct interaction.” Unattended runs on Pi are yours to build with ordinary tools, not a first-party feature you’re missing by accident.

    The stop-gate, tool-shaped. The pseudocode above is the contract; the wiring is per-tool. In Claude Code it’s a Stop hook in settings.json pointing at a verifier script that returns a block decision with the failure in the reason field:

    {
    "hooks": {
    "Stop": [
    { "hooks": [ { "type": "command", "command": "scripts/verify.sh" } ] }
    ]
    }
    }

    Codex and Cursor declare the equivalent in their own hook config; OpenCode needs a plugin; Pi needs an extension. Same two mandatory parameters everywhere.

    The spec-then-loop pattern has ready-made harnesses that build a version of the state-on-disk trio in for you - best known is the Ralph family. ralph-claude-code (as of mid-2026) wraps repeated agent runs against a PRD with exit detection, rate limiting, and a circuit breaker, spawning a fresh instance per iteration so state lives in files rather than a growing conversation. A harness handles the loop mechanics; the spec, the walls, and the worst-iteration question stay yours.

    Is auto-merge ever OK? As an earned escalation, narrowly: after a stretch of clean runs, with CI carrying the review checklist (suite evidence, secrets scan, scope check against the allow-list) - and only for change classes where the worst merged mistake is cheap to revert. A docs run or a lockfile bump can earn auto-merge; anything touching behavior keeps a human between green and main.

    The task needs my credentials - now what? Then the credential gets the same treatment as the file system: a scoped, disposable token - one repo, least privilege, revoked when the run ends - never your session, never an admin key. If the access genuinely can’t be scoped down, the task isn’t unattended-ready yet. That’s the prerequisite failing, not an inconvenience to route around.

    task.md skeleton (the frozen intent):

    # task.md - <slug>
    <what and why, in two sentences>
    Only touch: <explicit allow-list of paths>.
    Never touch: <migrations, CI config, this file's walls>.
    Done when: <a machine-checkable condition - a command and its
    expected output, not an adjective>.

    Companion files (append-only, created empty at kickoff):

    # progress.md - [timestamp] what was tried, what happened
    # decisions.md - [timestamp] what was chosen over what, and why

    Permissions wall (allow/deny split - the last deny rule protects the wall itself):

    {
    "permissions": {
    "allow": ["Read", "Edit(./src/**)", "Bash(npm test:*)", "Bash(git commit:*)"],
    "deny": [
    "Bash(rm -rf:*)", "Bash(git push --force:*)",
    "Read(.env)", "Read(.env.*)",
    "Edit(./migrations/**)", "Edit(<your tool's settings/config path>)"
    ]
    }
    }

    Cost wall: a per-run ceiling and a turn cap, in your surface’s units - Before you detach, step 3 has the three billing cases.

    Stop-gate: the pseudocode in While it runs, step 1, wired per the tool notes.

    Pre-flight: the three questions in Before you detach, step 4. Three good answers or you don’t leave.

    Copy all of it and you have the cage. What it can’t decide for you is whether this particular task is specified enough to survive your absence, and which rung this particular run has actually earned.


    The spec files become a library. Every watched run’s corrections are lines you’ll never type again, and the progress.md / decisions.md pair turns “what did last night’s run actually try” from a mystery into a grep.

    The vendors’ own postmortems - deleted remote branches, uploaded credentials, schema migrations attempted against production - stay other people’s stories, because you asked the three questions first. When the honest answers come back “almost nothing,” “a number I’d shrug at,” and “no,” walk away. That’s the discipline working.