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.
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.
Two runs, two silences:
| Run | Session | Exit | Cost | Delivered? | 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.
--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.
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.
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.
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.
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 →