Failure anatomy #1: the day the first autonomous run burned $2.04 and silently died

Our AI employee's first shift spent $2.04 — $2.0448678 as metered, rounded to cents here and unrounded everywhere it is counted — exited with a success code, and delivered nothing. The autopsy turned up three separate bugs.

AI-assisted content Post-mortem Measured, not modeled

On 2026-07-03 the scheduled worker — a headless claude -p run driven by a Windows Task Scheduler entry at 08:03 JST — was supposed to fetch IndieHackers, draft a batch of reply comments, run an audit over them, and deliver the result to Discord plus commit it to the repo. That is the whole job. No human is watching it run.

It did not deliver. And the way it did not deliver is worth writing down, because it failed quietly — the worst way for an unattended process to fail, since nothing tells you it happened. This is the anatomy of that morning.

The symptom

Two runs, two silences:

RunSessionExitCostDelivered?What you saw
08:03 scheduled 2a253300 rc=1 $0 no nothing in Discord
09:39 manual re-run 7d339a23 rc=0 $2.0448678 no looked like success

The 08:03 row is the honest kind of failure: it stopped early, spent nothing, and reported rc=1. The 09:39 row is the dangerous kind: it ran for 18 turns, spent real money, exited 0, and still shipped nothing — and its cost was very nearly lost from the record too. Three distinct bugs made that possible.

Bug 1 — a missing tool in --allowedTools halted the run before delivery

The headless runner was launched with --allowedTools WebFetch Read Write Bash Glob Grep TodoWrite Task — note there is no Edit in that list. The run did its real work: it built the draft batch and ran the zero-context auditor, which passed. Then it tried to stamp the audit record back into the draft file with an Edit call. Edit was not permitted, so the call was denied. It retried; denied again. Under its own two-strikes rule the agent then stopped — and it stopped before the Discord-send and commit steps that came after.

The result JSON recorded it plainly: two permission_denials for Edit on the batch file. The work was finished and audited; it just never left the machine, because a formatting step it did not strictly need was the thing standing between "done" and "delivered."

Lesson: for a headless agent, the tool allowlist is not a security detail you tune later — a single missing verb can silently truncate the run at an arbitrary point. Fix: add Edit to --allowedTools.

Bug 2 — PowerShell corrupted the run's stdout on redirect, nearly dropping the cost

claude.exe emits its result as UTF-8 JSON on stdout. The launcher captured it with a plain 1> result.json redirect. The problem: PowerShell decoded the child process's stdout using the console's OEM/ANSI code page before the redirect wrote the file — so every multibyte character (Japanese text, emoji, the arrows in the summary) was mangled on the way to disk. ASCII survived, which is what made it insidious: the file looked half-readable but was invalid JSON.

The evidence was exact. Both PowerShell's ConvertFrom-Json and .NET's JavaScriptSerializer failed at the same byte offset (1846) — precisely where the first corrupted multibyte string sits. The cost-capture step caught the parse error and logged "cost not captured," and the run's real $2.0448678 was about to be dropped from the ledger entirely.

Fix (two layers): (1) force UTF-8 at the top of the script — [Console]::OutputEncoding and $OutputEncoding set to UTF-8 — so the CLI's real bytes are preserved on capture; (2) a regex fallback in the catch block: the numeric fields are pure ASCII and survive even a corrupted file, so the real figures are recovered instead of dropped. Recovery was verified against the actual corrupted file (cost 2.0448678, in 7604, out 20354, 18 turns — all correct). A final PARSE-FAIL sentinel row was added for the case where even regex finds nothing, so a miss is never a silent gap and never a fabricated $0.

Bug 3 — exit code 0 masked the whole thing

Both bugs above were survivable on their own. What made the morning genuinely dangerous is that the halted 09:39 run reported terminal_reason: "completed" and exited 0. To any watching process — and to the ledger, and to a human glancing at the scheduler — exit 0 is indistinguishable from success. The agent had stopped early at a denied Edit, shipped nothing, and still handed back the same status code a clean delivery would.

This is the failure mode that unattended automation has to design against directly: a process that fails successfully. You cannot trust the exit code alone.

Fix: the launcher no longer trusts rc=0. The part shipped on the day was the alert: one line to Discord on rc!=0 or "is_error":true. The second half came later — on a clean exit the launcher now also parses a machine-readable STATUS: line the runner must emit as its final line (DELIVERED, STAND-DOWN, or FAIL), and an exit 0 with no readable status is treated as suspect rather than as success. That status check was hardened on 2026-07-23, three weeks after this incident, and for an honest timeline it belongs to that date rather than this one. Silence is no longer an accepted outcome.

Footnote — the 08:03 run

The earlier rc=1 run had a simpler, separate cause: the box's Claude OAuth session had expired sometime overnight, so the worker never got past authentication — its result JSON literally said "Not logged in · Please run /login". It is treated as environmental (and expected to recur), which is exactly why Bug 3's fix matters: the next silent logout now self-reports instead of vanishing.

What happened next

The rule this incident produced — never trust an exit code — did not hold. Two weeks later the same system ran six consecutive nights, exited 0 every time, charged $3.62, and shipped nothing; one of the causes was the identical same-file redirect bug found here, left unswept in another part of the same script. Read Failure 002 →

This experiment sells the prompts, launcher scripts, and guardrails behind these post-mortems — the same ones that turned this failure into three fixes. See the playbook →