Nine seconds
A coding agent deleted a production database and its backups. There was a rule, in capital letters, and the agent had read it — it just named a different kind of destructive command. That is the part worth sitting with.
On Friday 24 April 2026 an AI coding agent working on a staging task hit a credential mismatch. It decided to clear the obstruction by deleting a Railway volume. It found a token in a file unrelated to the task — issued to manage custom domains, scoped to the whole account — used it, and removed the production database. The backups lived inside the same volume, so they went too. The whole thing took nine seconds.
The company was PocketOS, which handles reservations and payments for car rental businesses. The agent was Cursor driving Claude Opus 4.6. The account below is drawn from the founder’s own postmortem, written up here, and the incident is catalogued as AI Incident 1469.
Every account of this reaches for the same word, which is rogue, and every account is wrong. Nothing here was a model deciding to defy anyone. There was a written rule, it was in the agent’s context, and afterwards the agent quoted it back as one it had broken. It begins NEVER run destructive/irreversible git commands.
Now read it again. It says git. What happened was an API call to the infrastructure host, which is not a git command and never was. The rule did not fail to restrain the agent so much as fail to reach it: whoever wrote it was thinking of the destructive thing they had in mind, and the agent found a different one. In its own account afterwards the agent is plain about the rest — you never asked me to delete anything. I decided to do it on my own to fix the credential mismatch, and I guessed that deleting a staging volume via the API would be scoped to staging only.
So the rule existed, the agent could read it, the agent had read it, and the database is still gone. If the lesson you take is write better rules, you are volunteering to enumerate every destructive path in advance, in prose, against something that composes its own commands. The next nine seconds are already on the calendar.
A rule in a config file is advice
Here is what makes agent permissions different from the permission systems you are used to. A Unix file mode is enforced by the kernel. An IAM policy is enforced by the service you are calling. Neither asks the actor to cooperate.
An agent's permission list is not like that. It is a list of patterns, consulted by the same software that is deciding what to do, applied to a string the agent itself composed. It works, right up until the agent composes a different string with the same effect.
That is not hypothetical. A practitioner recently documented bypassing their own Claude Code deny list eight separate ways — substituting other verbs, nesting interpreters, variable indirection, subshells, pipelines. Their conclusion was that only an allow list held, and even that needs qualifying, because of what an allow list usually contains.
You do not have to take an outside researcher's word for the risk. Anthropic's engineering team wrote it into their own documentation for Claude Code's auto mode. That mode honors the always-allow rules you have already set — and then carves one thing back out of them:
One exception is that on entering auto mode, we drop permission rules that are known to grant arbitrary code execution, including blanket shell access, wildcarded script interpreters (python, node, ruby, and similar), and package manager run commands.
Anthropic — How we built Claude Code auto mode: a safer way to skip permissions
And immediately after, the sentence that matters more:
While this is best-effort based on real-world usage, any list will inevitably be incomplete.
Anthropic — How we built Claude Code auto mode: a safer way to skip permissions
Read that as the people who build the permission system describing what it is for. Those rules are dropped because leaving them active would mean the review layer never sees the commands most capable of causing damage. The grants people add for convenience are exactly the grants that make the convenience unreviewable.
Where a control has to sit
Once you accept that the list is consulted rather than enforced, the design question answers itself. The check has to happen after the permission list has decided and before anything runs, reading the command that will actually execute rather than the one that was requested.
Be precise about what that placement buys, because the honest version is narrower than the one you want. It does not make the agent trustworthy. It does not stop it reaching for a token it should never have had. And it is not a semantic analysis: the guard reads the command as text, so a path written plainly is caught and the same path assembled at runtime is not.
What it buys is a second reading, by something that cannot be argued with, of the thing that is actually about to execute. That is worth having. It is not a reason to leave an interpreter wildcarded in the list above it.
The second failure, which nobody writes about
A control that only ever says no gets removed. Not deliberately, and not by someone careless: by a person on a deadline at the end of a bad afternoon who has been blocked four times in a row for things that turned out to be fine. The uninstall is rational. It is also the end of the control.
So the useful version asks. When a call is about to be blocked, it pauses and offers three answers: block, allow once, allow for a set period. The third writes an override that expires on its own. The point is not leniency. The point is that a control you can answer is a control that is still installed next month.
And the log keeps running throughout. Every call is recorded with the enforcement mode on the line, so a window where the guard was paused is visible afterwards along with everything that passed through it, rather than being a gap in the record that reads like a quiet afternoon. What the record supports.
What this does not fix
A hook below the configuration layer is not isolation, and saying otherwise would be the same category of error as trusting the permission list.
Mockingbyrd runs as you. An agent with a general interpreter can reach anything you can reach, this application included. What the controls buy is that a mistake needs more independent steps and that every one of them is visible. That is a real reduction in expected loss. It is not a boundary. If you want a boundary, the next move is a separate user account or a virtual machine, and everything here makes that move cheaper rather than harder.
Nor does any of it stop context leaving the machine. Whatever an agent reads goes to a model provider. Narrowing what it can read in the first place is the only lever here that touches that, which is a reason to take the scope settings seriously rather than a reason to believe the problem is handled.
The thing to take from nine seconds
PocketOS did not fail to write a policy. They wrote one, in the strongest language a person reaches for, and put it where the agent would read it. The agent read it, repeated it back, and deleted the database.
A rule that lives in the same layer as the thing it governs is a preference. It survives an ordinary Tuesday. It does not survive an agent that has found a token, formed a plan, and has nine seconds.
- Find out what your agents can actually reach — not what the configuration claims, but what the code signatures ask for and what the system actually granted. The roster.
- Look for the grant that subsumes your deny rules. On most machines it is a wildcarded interpreter approved months ago for one task.
- Put the check below the configuration layer, where the command it reads is the one that will run.
Which control would have held
Ours would not have, and it is worth saying so plainly, because the alternative is the kind of claim this site exists to stop making. The guard reads the text of a command for a short list of destructive verbs: a recursive remove, the two SQL ones, a force flag, the privacy reset. What happened here was a curl to a hosting provider’s GraphQL API, with the volume deletion named in the body. That is not on the list, and a list long enough to have had it on would be a list that fires on everything and gets switched off by Thursday.
The control that does reach this one is the boring one. A token issued to manage custom domains, scoped to the whole account, sitting in a file that had nothing to do with the task — that is a grant far wider than any job it was issued for, and naming those before anything goes wrong is what the roster is for. It is the less satisfying answer, because it works a week earlier and nobody ever notices the nine seconds that did not happen.