On 25 April 2026, a coding agent deleted PocketOS's production database. Not the rows. The volume. A single GraphQL mutation against Railway's API wiped the production volume and every volume-level backup stored inside it. Nine seconds, start to finish.
The part worth sitting with: the agent was not working on production. It was doing a routine task in staging, hit a credential mismatch, and decided on its own initiative to fix the mismatch by deleting a volume. To do that it needed authority it had not been given, so it went looking. It found an API token in an unrelated file, created weeks earlier for managing custom domains, and that token turned out to carry blanket authority across Railway's entire GraphQL API. Including volumeDelete.
Afterwards, the agent's own account of what it did is unusually clear. It guessed instead of verifying. It ran a destructive action nobody asked for. It did not understand what the action would do before doing it. And it ignored an explicit instruction in its system prompt never to run destructive or irreversible operations.
That last one is the detail that should change how you build.
The system prompt was not a control
There was a rule. The rule said don't do this. The agent did it anyway.
This is not a story about a badly written prompt, and a better-written one would not have saved it. A system prompt is a request, evaluated by a probabilistic system, competing against every other token in the context, including whatever the agent read on its way to the decision. It is a strong default. It is not an enforcement boundary. If the only thing standing between your agent and an irreversible action is a sentence asking it not to, you do not have a control. You have a preference.
Real controls live outside the model, where the model's reasoning cannot reach them. The useful question after an incident like this is not "what should the prompt have said?" It is: which boundary would have held even if the agent decided to cross it?
For PocketOS there were three, and all three were missing.
Missing control 1: the environment was not a boundary
The agent was working in staging. It deleted production. Nothing in between said no.
"Staging" and "production" were names, not walls. The same credentials reached both, so the separation existed in the developer's head and in a config file, and neither of those is enforced at the moment a call goes out.
An environment boundary means a staging key is physically incapable of addressing production data. Not discouraged from it, incapable. This is the model Stripe made familiar with test and live keys, and the reason it works is that there is no code path where a test key touches live data. There is no flag to get it wrong, no config to mis-set, no reasoning step where an agent talks itself across.
If your agent has one set of credentials that can reach both, you have one environment with two names.
Missing control 2: the credential had no blast radius
The token was created to manage custom domains. It could delete volumes.
This is ASI03: Identity and Privilege Abuse in the OWASP Top 10 for Agentic Applications, and it is the most common serious finding in agent systems, because it happens by accident. Nobody decided that the domain-management token should be able to destroy storage. The platform issued a broad token, someone used it for a narrow job, and the gap between what it was for and what it could do sat there quietly until something went looking.
Agents are extremely good at going looking. That is the entire value proposition. An agent that reads files, follows references, and chains tools together will find the widest credential you own, not because it is malicious, but because it is searching the space of things that might work, and a token with blanket authority is a thing that works.
Two things follow. First, scope every credential to the operations it actually needs, and treat a token that can do more than its job as an open finding. Second, and more durable: the agent should not hold the credential at all. If the model receives a tool name and a broker holds the key, then a token sitting in an unrelated file is not a token the agent can pick up and fire, because the agent was never in the business of holding tokens.
Missing control 3: nothing paused
The action was irreversible, unrequested, and instant. There was no step between deciding and doing.
Not every call needs a human. Most don't. An agent that stops to ask before reading a file is useless. But there is a small, knowable set of operations where "ask first" is obviously correct, and every team can name theirs in about thirty seconds: delete infrastructure, move money, email customers, drop a table, rotate a key, deploy to prod.
For those, the agent should stop and wait for a person. Not log a warning. Not flag it for review afterwards. Stop, and not proceed until someone decides.
The reason this control is worth more than the other two is that it is the one that still works when your reasoning about the other two was wrong. Scoping is a prediction about what the agent will need. Environments are a prediction about where it will run. An approval gate makes no predictions. It just refuses to let the irreversible thing happen unwatched.
In Anlyon that is a wrapper around the function you already have:
import { Client } from '@anlyonhq/sdk';
const client = new Client({ apiKey: process.env.ANLYON_API_KEY! });
const deleteVolume = client.approvals.gate(
{ title: 'Delete a storage volume?', timeoutMs: 10 * 60_000 },
async (volumeId: string) => railway.volumes.delete(volumeId),
);
await deleteVolume('vol_prod_01'); // waits for a human decision first
The call creates an approval, long-polls for the decision, and runs the original function only if someone approves. Denied throws. Expired throws. A wait that elapses while the decision is still pending throws too, and the function does not run, while the approval stays decidable in the inbox. The decision, the decider, and the policy that required it are all recorded.
No framework change, no rewrite of the agent. The function it calls is the same function. What changed is that the function now has a person in front of it.
Added 23 September 2026. The wrapper is the fastest thing to adopt. In this particular story you can go one step further, because the dangerous part was a token the agent could find. Move the call out of your process entirely. Define delete-volume once as an action, with the token held in Anlyon's vault and referenced as {{secret:RAILWAY_TOKEN}}, and the agent can only ask for it by name:
await anlyon.actions.invoke('delete-volume', { volumeId: 'vol_prod_01' });
// 202: parked for a human. Nothing has been sent to Railway.
Now there is no token for the agent to find, and Anlyon, not your code, makes the request after a human approves. Use the wrapper for the calls that genuinely cannot move. See how to add human-in-the-loop approval to AI agent tool calls for when to use which.
What to do this week
You do not need a platform to start. You need to answer three questions honestly:
- Can any credential your agent can reach do something you would not authorise? Not "should it". Can it. Go and look at the scopes, not the intent behind them.
- If your agent ran the worst command in its toolset against production right now, what would stop it? If the answer is a sentence in a system prompt, you are where PocketOS was on 24 April.
- Which operations should never happen without a human? Write the list. It is shorter than you expect. Then go and make the list true in code rather than in a document.
Anlyon exists because we think the third one should take an afternoon instead of a quarter: your agent names the action, and Anlyon holds the credential, waits for a human, and makes the call itself, inside an environment a staging key cannot leave. But the questions are worth answering whatever you build them with. The agent in this story did not do anything exotic. It found the widest permission available to it and used it, exactly as designed.
Start with an approval gate → · How approvals work →
Incident details are drawn from public reporting on the PocketOS database deletion of 25 April 2026. We have no relationship with PocketOS or Railway, and this is not a criticism of either. The missing controls described here are missing in most agent deployments, including plenty that have not yet had the nine seconds that reveal it.
