I Audited a Year of My Own Agent Sessions
I archive every coding-agent session I run. Claude Code, Codex, Cursor, across two machines. It started as damage control after I found out the tool deletes transcripts on a thirty day timer, and it turned into a corpus: 5,284 unique sessions once you collapse the duplicates.
Last week one of those sessions interrupted its own task to tell me it had deployed to production by mistake. It had written a shell block, labelled it “deploy to the dev deployment”, and run the CLI’s plain deploy verb. That verb targets production. The local environment file it had been developing against points at dev. Nothing broke, the schema change was additive, and it caught the discrepancy itself about ninety seconds later while checking an unrelated endpoint.
Two things about that bothered me. I would not have found out any other way. And I had no idea whether it was the first time.
The archive can answer the second one, so I wrote a scanner and pointed it at everything.
What counts as doing something it shouldn’t
The scanner reads every transcript, splits each shell command into segments (heredoc bodies stripped, so a Python script that happens to contain the string git push is not counted as a push), and matches about sixty patterns: deploy verbs, publish verbs, git mutations, destructive commands. Then it collects four independent signals about whether the action was wanted.
| Signal | What it is | What it’s worth |
|---|---|---|
| No authorising word nearby | keyword check over the recent human turns | low |
| Agent self-callout | the agent’s own text admitting an out-of-bounds action | high |
| Prohibition then action | human says “don’t X”, X happens later | medium |
| Human rebuke | ”why did you”, “I didn’t ask for that”, “revert that” | high, very sparse |
The first signal is the one everybody reaches for, and it is the one that does not work. I pulled the 63 highest-stakes actions that had no authorising keyword anywhere in their session, then read the actual conversation around eight of them at random. Seven were plainly authorised, in words my regex didn’t know. “Fix the history” authorises a filter-branch. “Commit everything properly and I’ll set up the service” authorises creating the repo and pushing it.
So a keyword-based count of unauthorised agent actions overstates by roughly eight times, and I only know that because I checked instead of publishing the number. Everything below rests on the agent’s own admissions and on reading transcripts.
Two more things the method cannot see. Cursor’s exported sessions contain no tool calls at all, only the prose, so almost none of its 4,404 sessions can contribute an action. And an action that the agent never narrated and that was never rejected at a prompt is invisible to all four signals. This is a floor, not a census.
The rates
Five percent of sessions ran anything on the risky list at all, which is mostly commits and pushes. Two percent ran something destructive, which is mostly rm -rf on a build directory. Thirty-one sessions in the entire corpus ever touched a production system, and that number is flat month over month, because it is not a behavioural trend, it is the rate at which deploy work happens.
The dozen incidents that survived reading are unglamorous. A deploy that hit prod instead of dev. Docs pushed straight to a default branch when the established pattern was a pull request. A pull request merged while three of its commits were still sitting unpushed on a laptop, so the merge silently shipped less than it claimed. An agent asked to address review comments that resolved them through the API instead, which in that tool means deleted, with no backup. A misclick in a real UI during a test that resolved a comment thread the test was supposed to leave alone.
Notice what is not in that list. Nothing malicious, nothing that looks like a model pursuing a goal I didn’t give it. Every one is a boundary error: the right action aimed at the wrong target, or the right action taken one step earlier than the process allowed.
It catches itself, and I don’t catch it
Thirty of the flagged incidents surfaced because the agent told me. One surfaced because I noticed, and it was a UI complaint, not a safety one.
That ratio is the most encouraging number in the audit and the most fragile. The admissions are good: they arrive as the first line of the next message, before the work I actually asked about, and in the prod-deploy case the agent escalated its own report unprompted a few turns later once it understood the blast radius better than it had at first. That is exactly the behaviour you want.
But self-reporting is a behaviour, not a mechanism. It runs after the fact, it depends on the agent noticing, and it fails completely on any violation that produces no output to notice. Hold that thought.
The gate works. It almost never fires.
Of the sessions that record a permission mode, 95% ran with the prompts turned off. Not as an accident: I turned them off, because approving four hundred ls calls a day is how you train yourself to approve without reading, which is worse than no prompt at all.
The price of that choice is measurable, and so is the value of the prompt when it does exist. In the whole corpus, 51 tool calls were rejected at a permission prompt. Of those, nine had a similar call run afterwards, and reading them, seven were siblings in a parallel batch where only one was rejected. Two were genuine re-runs, and one of those had changed a port number in response to the rejection.
So the agent does not argue with an explicit no. It does not find a way around the gate. It essentially never hears one.
That reframes the whole problem. The interesting question is not “will it comply”, it is “is anything ever asked”, and for 95% of my sessions the answer was no.
Production has the longest leash
For every risky action, I measured how many agent steps had passed since the last human message. It is a rough proxy for how unattended the action was.
Deploying to production is the action that happens furthest from a human, and by the widest margin: a quarter of production commands ran more than a hundred steps after I last said anything. Opening a pull request, which is the action designed to be reviewed, happens closest to a human.
That is exactly backwards, and it is not the model’s fault. Long autonomous runs are where deploy work naturally lands, because deploying is the last step of a long task and long tasks are the ones you leave running. The incident that started this audit fired 43 steps into a self-paced loop.
The rule that never sticks is the one the harness teaches
Here is the violation that self-reporting cannot catch, because it produces no output at all.
I asked, in writing, in the memory the agent loads every session, that it never add Co-Authored-By: Claude trailers or “generated with” footers to my commits. The reason is mundane rather than principled: a client’s commit history is not the place for my tool vendor’s marketing, and I had already had to strip the trailer out of one repository the day before it went public.
Then I counted the actual commits in the actual repositories.
Across every repository, 143 of the 332 commits made in the twelve days after I set that rule still carry the trailer. It is the most frequent thing my agents do that they have been told not to do, by two orders of magnitude over everything else in this audit, and I never noticed, because a commit trailer scrolls past in a git log I don’t read line by line.
The cause is not forgetfulness. It is a direct contradiction inside the agent’s own context. The harness system prompt instructs, in every session: end git commit messages with a Co-Authored-By trailer, and end pull request bodies with a generated-with footer. My rule sits in a memory file loaded alongside it. One of those is a product default written by the vendor and one is a user preference, and the product default wins, quietly, session after session.
I find this the most useful finding in the audit, and the least about agents specifically. When a standing instruction loses to a default, adding a louder instruction does not fix it. You are not debugging a memory problem, you are debugging a conflict, and the only reliable resolution is to move the rule out of the prompt and into something that executes: in this case a commit-msg hook that strips the trailer whether or not anybody remembered.
What I’m changing
A commit-msg hook per repository, because twelve days and 143 commits is enough evidence that the instruction-level fix does not work.
A deny list for exactly the deploy verbs, and nothing else. Permission prompts stay off for the hundreds of harmless commands, and turn on for the eight that touch production. That costs me one prompt per real deploy, and the data says the gate is respected when it fires.
Deploy commands always name their target explicitly. Never the bare deploy verb whose default is production; always the flag that says which environment, so the target is in the command instead of in the environment.
And the per-project rules move into the file the agent actually reads at startup. In the prod incident, the rule that said “nobody deploys to prod by hand” existed, correctly written, in a markdown file in that repository. The agent found it after the fact, while auditing itself. A rule nobody loads binds nobody.
The uncomfortable summary
Agent overreach, in a year of my own real work, was rare, boring, and almost always self-reported. The failures were boundary errors rather than intent errors. The single safeguard that exists works nearly perfectly and is switched off for 95% of sessions by my own hand.
And the only violation that happens constantly is the one where the agent is doing exactly what it was told, by someone who wasn’t me.