The worst agent incidents are the ones nobody narrates
An unattended agent reads my own coding-agent transcripts, decides which moments were real incidents, and files them in a ledger, exclusions included.
Every mistake a coding agent makes ends up in its transcript: the secret it printed, the file it deleted, the afternoon it spent blaming a library for its own bug. Nobody rereads transcripts, so the lessons get lost with them. ai-incidents is a scheduled agent that reads my own transcripts, judges which moments were real incidents, and files them in a ledger ranked by severity, along with a record of what it chose not to file. Since July the ledger has grown to 47 entries, 32 of them filed by the sweep itself.
What it does #
Once a week the sweep reads the new transcripts on the machine. If nothing looks like an incident, it stops without loading a model. Otherwise a judge model reads the candidates alongside the existing ledger and adds each confirmed incident to the top:
## <short title> · <YYYY-MM-DD> · <HIGH|MEDIUM|LOW>
- **What:** <one sentence — what the AI did>
- **Cost:** <one sentence — what it actually cost>
- **Lesson:** <one actionable sentence — how to not repeat it>
The bar is a real cost: data loss, an outage, a leaked secret, a destructive action, a debug spiral, the wrong tool, or spend nobody was metering. Routine iteration doesn’t count, and neither does an agent catching its own mistake before it cost anything. Each run also writes a dated report listing every excluded candidate with a one-line reason, so the bar stays visible and arguable. I get a push notification only when something was filed.
I looked for prior art and didn’t find anything else that does this.
How it works #
The collector is a read-only Python scanner over Claude Code’s JSONL transcripts. It’s incremental, and its first run on a host only records a baseline. A candidate is either a snippet, where the user blamed the agent or the agent admitted a mistake, or a digest, where nobody said anything but the session ran destructive commands or something notably broke. A bash gate runs the collector and wakes the judge only if there are candidates. Before the gate, a quiet run cost about 2.1 million tokens, measured, just to load an agent that ran one command and found nothing.
The judge runs on Sonnet. Its envelope allows the file tools, date, cp, the collector, the notifier and one publish script. The sweep reads every transcript on the machine, so it’s the last thing that should hold a general git grant. The publish script is its only way to commit or push. It takes a commit message and nothing else, and it checks that the remote really is the ledger repo, because a repointed checkout would otherwise send a private record somewhere else. On a recent run the judge tried to read the collector’s source and was denied. It noted the denial in its report and finished.
Why a model judges instead of rules #
The scanner is deliberately high-recall. It fires on rm -rf, git reset --hard, pkill, “I broke” and “why did you”, and most hits are harmless: a job clearing its own scratch directory, a deliberate revert, a process I asked it to stop. What matters is whether it cost anything, whether it was the agent’s doing, and whether it’s already filed under another title. Regexes can’t answer that. One --no-verify commit was excluded because no hooks were installed, so it bypassed nothing.
Misattribution settled it: blaming a tool or upstream project for a problem the agent caused, and acting on that. It hides itself, because a bug that has been successfully blamed on someone else is a bug nobody looks for again. It also trips none of the obvious signals. There’s no confession and no complaint, just a version pin or a workaround. The scanner watches for the language of blame, and the judge is told to check what the session did to the accused tool first. The case it was built around: an agent wrote MaxAge=9d # comment into a timer and my monitoring went blind. The easy reading was that node_exporter is strict about inline comments. Actually, my own parser was treating the comment as part of the value.
The judge can’t weigh evidence it’s never shown #
The first collector matched about twenty confession phrases. On 14 July one session blinded my monitoring for about twenty minutes, committed 14 embedded git repos to main, and spawned a runaway agent that had to be killed. The collector missed all three, because nobody used the magic words.
So the scanner started reading tool calls, and the first run still threw the 14-repos incident out for lack of evidence. The judge was right: it had never seen any. The session had also confessed to other things, so it took the snippet path and its commands were never shown. Now every session with snippets gets its commands attached too. The damaging command, git add -A, exited 0 and matched no destructive pattern. The proof was in its output, warning: adding embedded git repository fourteen times, so the scanner now reads the output of successful commands as well. And the evidence list was capped at eight commands, which it filled with the first eight: scratch-directory churn. It now keeps the worst:
def danger_rank(cmd):
c = cmd.lower()
if re.search(r"git\s+(rm|reset\s+--hard|checkout\s+--|push\s+(-f|--force)|commit\s+--amend|clean)", c):
return 0 # rewrites history or discards work
if re.search(r"(zfs\s+destroy|drop\s+(table|database)|delete\s+from|truncate|shred|>\s*/dev/)", c):
return 0 # data destruction
if re.search(r"rm\s+-[a-z]*[rf]", c) and not re.search(r"/tmp|/var/tmp|scratch", c):
return 1 # deleting something that is not scratch
...
I filed that incident by hand.
Not losing what it hasn’t judged #
On 13 July the judge died on a spend limit. The scanner had already marked those transcripts as read, so the candidates were gone. And because my transcript reaper deletes only what the sweep has read, the sources had just become deletable. Seen-state is now two-phase. Candidates are staged as pending and promoted only after a successful publish, so a failed run leaves them for the next one.
Three from the ledger #
A partial update that was a full replace (HIGH). An agent called the NAS’s app-management API with only the environment variables it meant to change. The API replaced the whole config, blanking the app’s database password, its Redis password and the encryption key protecting every credential the app stored. The app was down about eight minutes, and the key came back only through root access to the old container. Lesson: treat a partial update as a full replacement until proven otherwise.
Cleanup that matched too much, twice (HIGH). While tidying up its own test sessions, an agent filtered the shared session list by name and time window and deleted the matches, including my real sessions and the one I was typing into. It reported the mistake, then did it again the same day. At least five sessions were destroyed beyond recovery. Lesson: delete only what you can prove you created.
Deleting the evidence of its own bug (HIGH). An agent decided a price-history file was corrupt, with “905 missing trading days”, and deleted it. The corruption was its own validation bug: a parquet round-trip had renamed the date column, which broke its comparison. The free source returned nothing and the paid one was rate-limited, so the data stayed lost. Lesson: quarantine suspect data instead of deleting it, and check a corruption diagnosis against a known-good sample before acting on it.
What it taught me about working with agents #
- Confession isn’t a signal. The costly incidents were the ones nobody mentioned, and exit code 0 looked fine while the output said otherwise.
- Repeated mistakes need mechanisms, not reminders. A pattern that recurs three times gets its own line in the ledger, and the fixes that stuck were mechanical: a wrapper, a gate, a check. After a gate preview spawned a real agent, every gate got a dry-run flag.
- The judge still needs a person behind it. It wrongly excluded two real incidents in its first days. Its prompt tells it to be strict and honest, “including about incidents caused by Claude sessions much like this one”, and several entries are about the fleet’s own agents.
Status #
As of 20 September 2026 the ledger has 47 incidents: 17 HIGH, 16 MEDIUM and 14 LOW. It was seeded on 11 July from about 113 transcripts. The sweep runs weekly on the dev VM as a job in my claude-fleet control plane. It ran nightly for a while, but because the collector is incremental, a weekly run judges the same candidates in one batch. A recent week scanned 362 new transcripts, passed 50 candidates to the judge (the per-run cap) and filed three.
Limits: it reads only Claude Code transcripts, and anything past the cap waits a week. The judge sees snippets rather than whole transcripts and is told not to invent detail, so thin evidence turns into an exclusion. The ledger itself stays private because its entries describe my own systems.
What’s next #
The sweep is being released as a standalone tool. If I started again, I’d build the command-reading scanner and the two-phase state first, because I learned both by losing candidates. A redacted public cut of the ledger exists, but it lags the real one, and I cut it by hand on purpose, because deciding what’s safe to publish is a judgement call, not a diff.
Links #
- github.com/sudolulo/ai-incidents: the standalone sweep.