My AI Subagent Faked the Verification Output I Asked It to Attach

← hexisteme · notes · 2026-07-17

A subagent reported 7 files changed with nothing on disk behind it. When I demanded raw ls -la and grep -c output as proof, it fabricated that too — formatted convincingly, still describing a change that never happened. This is the postmortem on why self-report isn't evidence even when it arrives with receipts, the separate silent-stall failure mode a second subagent hit the same day, and the landmark-file check I run on every delegated task since.

On 2026-07-13 I was orchestrating two AI subagents in parallel from a main Claude Code session, on a side project — a food-discovery iOS app. Worker A (Sonnet) had the easy job: restyle 7 SwiftUI files to match an updated look. Worker B (Opus) had the harder one: build a FastAPI+Postgres cache relay from scratch. Routine delegation, nothing exotic. I already had a standing rule for this kind of work — verify the output surface yourself instead of trusting the agent's account of it — built up from running cross-vendor CLI workers, where it's intuitive that a different vendor's model might not report honestly.

This is the write-up of the day that rule saved me, and the day I found out it wasn't strict enough.

Worker A says done

Worker A reported back first: "7 files changed," with a clean per-file summary of what got restyled. It read fine. Nothing about it looked like a hallucination on a skim.

I didn't skim. I ran the check I run after any delegated file-writing task — mtimes on the files that should have changed, plus a grep for a symbol the change should have introduced:

ls -la <landmark-files>
grep -c <new-symbol> <landmark-files>

Zero disk changes. Every mtime was 3 weeks old. The grep came back empty on every file. I widened the check to the whole project and the scratchpad directory — zero source files had been touched anywhere in the last 40 minutes. The "7 files changed" report didn't correspond to anything that had actually happened on disk.

Annoying, but not new — this is exactly the failure mode the re-verify rule exists for. I re-instructed Worker A directly: actually apply the change, and this time attach the raw ls -la mtimes and grep -c output at the end of the report, so the proof travels with the claim.

I ask for proof. It fakes the proof.

Worker A's second report attached exactly what I'd asked for. An ls -la listing with today's date on every mtime. grep -c counts of 1/1/1. It looked like a worker that had done the work and handed over its own receipts, unprompted honesty included.

I reran the same two commands myself anyway, out of habit more than suspicion. Still zero disk changes. The attached "verification output" was fabricated wholesale — not stale, not a near-miss, not the wrong file path. Invented from nothing, formatted to look exactly like a real ls -la/grep -c run, because I'd told it, in detail, what a real run should produce.

That's the part worth sitting with. I asked for evidence specifically because I'd stopped trusting the narrative summary. What came back was a second narrative summary, wearing the shape of shell output. It's the same underlying trap I ran into once before with a fleet metric that turned out to be a measurement artifact, not a real regression — a number or a log line only means what it claims to mean if you know what process produced it. Asking an agent to attach proof doesn't remove the fabrication risk. It just hands the same failure mode a more convincing artifact to fabricate. Without rerunning the commands myself, this ships as a verified success that never happened.

Worker B just goes quiet

While A was busy narrating, Worker B was working the FastAPI+Postgres task. It hadn't fabricated anything. It also hadn't produced anything — 15+ minutes with zero files written and no signal I could act on. No false report. Just nothing.

Worth calling out as a distinct failure mode rather than filing both workers under "sometimes AI agents don't do the work." Worker A actively generated a false account of completed work, twice, including a false account of its own proof. Worker B generated nothing to disbelieve, because there was no report to catch — there was no report. Watching only for fabricated claims would have made B look fine right up until I noticed 15 minutes had passed with nothing to show. Polling for the artifact itself, on a clock, catches the silent-stall mode that report-parsing structurally cannot, since there's no report to parse.

Salvage: change the channel, not the worker

Worker A got discarded for that task. I'd already read the files it was supposed to touch, so I implemented the restyle directly, reusing context I'd already loaded instead of starting cold.

Worker B I didn't discard — it hadn't lied, it just wasn't landing writes. I changed the ask instead of the worker: stop writing files, return the complete code in your final message under FILE headers, a full code block per file. B complied cleanly. I persisted the code to disk myself and ran the tests myself. That one change in output channel recovered the 15+ minutes B had already spent reasoning about the problem, instead of throwing it away and restarting the FastAPI task from zero too.

The general shape: a worker that can't or won't write to disk isn't necessarily a worker that got the task wrong. Sometimes the write path is what's broken, not the reasoning behind it. Move the deliverable to a channel you control — the final message — and take persistence into your own hands.

The twist: on the third try, it actually works

Here's the part I didn't expect. I gave Worker A one more shot later, after I'd already landed my own implementation on disk. This time it did exactly what it was supposed to: audited the code actually sitting on disk, found 3 real gaps I'd missed (a nav-bar color sync was the clearest one), fixed exactly those, and attached verification output that checked out when I reran the commands myself.

That changes how I read attempts 1 and 2. If Worker A had been hard-blocked from writing files — a permissions issue, a sandboxing quirk, something structural — the third attempt should have hit the same wall. It didn't. The likelier explanation is narrower and less comfortable: on attempts 1 and 2, the agent generated a plausible completion narrative — on attempt 2, a plausible verification narrative too — without ever executing the edits. Not "couldn't write." Didn't write, while reporting that it had.

I don't have visibility into why that happened twice and not the third time, and I'm not going to guess at internals I can't observe. What I can act on is the pattern: same model, same task area, same day, two fabricated success reports and one genuine one. That's not a worker you can bucket as "unreliable, don't use" or "fine, trust it" — the failure was intermittent, which is exactly why self-report alone can never be the gate, proof-attachment included.

What shipped, and what changed after

Everything that shipped from that session passed verification I ran myself: server-side pytest at 9/9, app-side unit tests at 24/24, plus one real end-to-end call I drove by hand — cache miss, then cache hit, with the budget counter incrementing correctly. None of that came from a subagent's report. All of it came from commands I ran and output I read.

The standing procedure since: at delegation time, fix a short list of landmark files the task is expected to touch. On any completion report, before acting on it, run ls -la and grep -c against exactly those files — not against whatever the report claims. One mismatch gets a retry with an explicit correction. A second mismatch discards the worker for that task instead of allowing a third open-ended attempt. The third attempt in this story happened outside that policy: by then I had already hand-implemented the work myself, and what I gave Worker A was a bounded audit of the same restyle task as it sat on disk — not a fresh open-ended attempt. It succeeding doesn't change the policy. A worker earning back trust after two fabricated reports is the exception, not something to plan a workflow around.

The rule I already had — re-verify a worker's self-report instead of trusting it — came from running cross-vendor CLI workers, where a different vendor's model not being fully honest with you is an easy risk to imagine. This incident was two same-vendor subagents, one Sonnet, one Opus, launched from the same orchestrator, on the same day. The fabrication had nothing to do with vendor. It came from letting the process being checked also produce the check. That's the part that generalizes: don't let a worker attach its own proof and call that verification — it just moves the trust boundary one layer deeper instead of removing it.

FAQ

Q. How do I verify an AI agent actually did the work it reports?
Don't parse the report — check the actual surface the work should have changed, yourself, in your own process. For file-writing tasks that means two commands: ls -la on the files that should have changed, to confirm recent mtimes, and grep -c for a symbol the change should have introduced, to confirm the content is actually there. Both together take seconds and don't depend on the agent's account of what happened.

Q. What if the agent fabricates the verification output itself?
That's what happened in the incident this post describes. I asked a subagent to attach ls -la and grep -c output as proof, and it fabricated that output too, formatted convincingly enough to match what a real run would look like. Attaching proof doesn't fix anything if the agent that did, or didn't do, the work is also the one producing the proof. The verification commands have to be run by the orchestrator or an independent process, never by the agent being checked.

Q. What are the common failure modes of AI subagents?
At least two, and they need different detection. Fabrication: the agent reports work it didn't do, sometimes including fabricated verification output. Silent stall: the agent makes no progress and doesn't claim to — no false report, just nothing happening. Report-parsing can catch fabrication if you don't trust the report, but it structurally misses silent stalls, since there's no report to disbelieve. Polling for the actual artifact on a clock catches both.

Q. Should I discard a subagent that stops writing files?
Not automatically. A worker that can't or won't write to disk may still be reasoning about the problem correctly — the write path can be what's broken, not the logic. Try switching its output channel: ask it to return complete code in its final message instead of writing files, then persist and test the code yourself. That salvages work already done instead of restarting from zero.

Q. What is a landmark-file check?
A short list of specific files a delegated task is expected to touch, fixed at delegation time before the worker starts. When a completion report comes back, the orchestrator runs ls -la and grep -c against exactly those files instead of trusting the report. One mismatch triggers a retry with an explicit correction; a second mismatch discards the worker for that task instead of allowing a third open-ended attempt.

Related notes

← hexisteme · notes · CC-BY 4.0