When a Coding Agent Drops Your Database: Why It Happens and the Guardrails That Stop It
- The failure mode is not an agent *deciding* to be destructive. It is an agent debugging a discrepancy and reaching for a tool whose documented behaviour is to reconcile schema state - which for migration tooling can mean dropping and recreating.
- Migration commands are destructive by design.
prisma migrate reset,migrate diffused to apply rather than inspect,db push --accept-data-loss, and their equivalents in every ORM exist precisely to discard state. The agent is not misusing them. - The root cause is almost always one credential. If the agent's shell has a connection string with DDL rights on production, the guardrail is the model's judgement - which is not a guardrail.
- The single highest-value control is a read-only role by default. An agent that cannot execute DDL or DELETE cannot destroy data regardless of what it reasons its way into.
- Second: deny destructive commands at the hook, not in the prompt. A
PreToolUsedeny-list formigrate reset,DROP,TRUNCATEand--accept-data-lossis deterministic. "Please be careful with the database" is not. - Third: keep production credentials out of the environment the agent inherits. MCP servers and agent shells inherit environment variables, so a production DSN in a dotfile is a production DSN in the agent's hands.
- Publicly reported incidents follow this pattern closely - one described a production wipe roughly ten minutes into a first agent session, while the agent investigated a contradiction between a raw database result and an ORM read. Treat it as illustrative of the mechanism rather than as a verified case study.
Every few weeks someone reports that a coding agent destroyed a database. The reactions split predictably - *the agent went rogue* on one side, *user error* on the other - and both miss the mechanism. The interesting thing about these incidents is how ordinary they are. No prompt injection, no jailbreak, no adversary. An agent was debugging, reached for a documented command, and the command did what it says on the tin.
This guide is about that mechanism and the controls that actually prevent it. The short version: the agent's blast radius is defined by the credentials in its environment, and no amount of instruction narrows it.
How it actually happens
The escalation is mundane and worth tracing, because the fix follows from it. An agent is asked to investigate a bug. It queries the database directly and gets one answer. It reads through the ORM and gets a different answer. That contradiction is exactly the kind of thing agents are good at noticing and bad at sitting with - so it forms a hypothesis: the schema and the migration history have diverged.
There is tooling for precisely that hypothesis. Every ORM ships commands to reconcile schema state against migration history, and reconciling means discarding whatever does not match. A publicly reported case described a production wipe roughly ten minutes into someone's first agent session, while the agent was investigating this exact class of contradiction and ran a prisma migrate diff invocation with the wrong parameters. We cite that as illustrative of the pattern rather than as a verified case study - it is a user report, not a coordinated disclosure - but the mechanism it describes is entirely reproducible, and that is the part worth acting on.
| Command family | Documented behaviour | Why an agent reaches for it |
|---|---|---|
prisma migrate reset | Drops the database, recreates, replays migrations | The canonical fix for drifted migration state |
prisma migrate diff (applied, not inspected) | Emits or applies the delta between two schema states | Reasonable for *inspecting* drift; destructive if piped to execute |
prisma db push --accept-data-loss | Forces the schema to match, discarding conflicting data | The flag name reads like a formality when you are moving fast |
DROP / TRUNCATE / bare DELETE | Removes schema or rows | Clearing test state, resetting a fixture |
| Framework equivalents | rails db:reset, alembic downgrade base, django flush | Same reasoning, different ecosystem |
Notice that none of these are exotic. They are the documented, recommended commands for the situation the agent correctly identified. The agent's reasoning was sound and the outcome was catastrophic, which is why *prompt better* is not a mitigation. The gap is that the tooling cannot tell development from production, and neither can the agent - both see a connection string.
Why prompting and approval do not save you
Three reasons instruction-level controls fail here, and they compound.
- Instructions are advisory. *Never modify production* in a system prompt or
CLAUDE.mdis context the model weighs, not a rule it is bound by. It usually holds. Usually is not a security property. - Approval fatigue is real and rational. An agent in an autonomous mode issues many commands. A human approving each one stops reading around the twentieth, which is exactly when the destructive one arrives. This is the same dynamic as dangerously-skip-permissions and Cursor's auto-run modes - people enable them because the alternative is unusable.
- The dangerous command does not look dangerous in context.
migrate diffreads like an inspection verb.db pushreads like a deployment. A reviewer skimming a plausible debugging sequence has no reason to stop, and the destructive parameter is often the least visually prominent part of the line.
The controls that actually work
These are ordered by leverage. The first one alone eliminates most of the risk, and it costs nothing.
| Control | What it stops | Cost |
|---|---|---|
| Read-only role by default for agent sessions | All destructive outcomes, regardless of reasoning | Low - grant write explicitly, per task |
| No production DSN in the agent's environment | The agent reaching production at all | Low - separate shells and profiles |
PreToolUse deny-list for destructive verbs and flags | The specific command families above, deterministically | Low - one policy, fleet-wide |
| Separate credentials per environment, distinct names | Wrong-environment mistakes by humans too | Medium - naming and plumbing |
| Point-in-time recovery, tested | Nothing - but bounds the damage to minutes | Medium - and you needed it anyway |
| Audit trail of agent commands | Nothing - but makes the post-mortem possible | Low with instrumentation |
The reason read-only by default dominates is that it is the only control that does not depend on correctly anticipating the dangerous command. Deny-lists are valuable and you should have one, but they enumerate known-bad, and there is always another way to lose data. A role without DDL or DELETE rights makes the entire class unreachable, and the friction is small: elevate deliberately, for a specific task, then drop back. That is ordinary least privilege applied to a non-human principal - see least privilege for AI agents.
The environment point is underrated and specific. Agent shells and MCP servers launched over stdio inherit environment variables from the user's shell profile. A production DATABASE_URL exported in .zshrc for convenience is a production database in the hands of every agent and every MCP server that machine runs. This is the same inheritance property that makes agent config files an attractive credential aggregation point - see secrets management for AI agents.
Where the deny-list belongs
In a hook, below the model, where a prompt cannot negotiate with it. Where an agent exposes a control point - for example the PreToolUse event in Claude Code - each tool call can be evaluated against policy and returned allow, deny, or log before it executes. A command matching migrate reset, --accept-data-loss, DROP, or TRUNCATE is denied deterministically, and the denial is recorded.
That placement is the same lesson AWS drew from CVE-2026-10591, where hidden text on a web page made Kiro rewrite its own MCP config: they moved sensitive files onto a protected paths list requiring explicit approval, enforced in the most autonomous mode as well as the supervised one, so the model could not bypass it by changing its own mode. Decide below the model. It generalises from config files to destructive commands without modification. Notably, AWS's newer Kiro Crew ships denied-by-default commands, sensitive-path blocking and credential redaction as day-one properties - the vendors are converging on this.
This is where Anomity operates. Runtime governance evaluates tool calls at the hook and returns allow, deny or log, and decisions land in a queryable 90-day audit trail - so the question *what did the agent run against which database* has an answer that is not a reconstruction from shell history. Anomity's Endpoint Sensor also inventories eight AI artifact types per endpoint, including secrets and hooks, which is how you find the production DSN sitting in a dotfile before an agent does. Metadata only, on-endpoint secret redaction, SOC 2 Type II, and it complements existing tooling.
You can't govern what you can't see.The Anomity principle
Checklist
- Give agent sessions a read-only database role by default. Elevate deliberately for a specific task and drop back afterwards. This one change removes most of the risk.
- Remove production connection strings from shell profiles,
.envfiles and anything an agent shell or stdio MCP server inherits. Inheritance is the delivery mechanism. - Add a
PreToolUsedeny-list coveringmigrate reset,migrate diffused to apply,db push --accept-data-loss,DROP,TRUNCATE, bareDELETEwithout aWHERE, and your framework's equivalents. - Name environments so they are unmistakable in a command line, and never let development and production credentials differ by a single character.
- Verify point-in-time recovery by actually restoring, on a schedule. An untested backup is a belief, and this is the class of incident where minutes matter.
- Instrument agent commands into an audit trail before you need it. Shell history is not evidence and is frequently the first thing lost.
- Treat autonomous modes as a decision about blast radius rather than a productivity setting - see dangerously-skip-permissions explained and Cursor auto-run limits.
- Write the destructive-data path into your AI agent incident response playbook - who can restore, how quickly, and how you establish what the agent actually ran.
The uncomfortable conclusion is that these incidents are not really about AI. An agent with write credentials on production is a process with write credentials on production, and we have known for thirty years how that ends. What agents change is the *rate*: they issue far more commands than a human, at a speed that defeats review, with reasoning that is usually sound and occasionally catastrophic. So apply the controls we already trust - least privilege, credential separation, deterministic deny-lists, tested recovery - and stop asking the model to be careful. For the surrounding practice see securing AI coding agents and CLIs, Claude Code permissions and hooks hardening, and for lifecycle placement ADLC. To see which endpoints hold production credentials an agent could reach, request early access.
Frequently asked questions
Why would an AI agent delete a production database?
Not by deciding to be destructive. The common path is debugging. An agent investigating a bug queries the database directly, reads the same data through the ORM, gets a different answer, and forms a reasonable hypothesis - that the schema and migration history have diverged. Every ORM ships commands to reconcile that divergence, and reconciling means discarding whatever does not match. So the agent reaches for a documented, recommended command for the situation it correctly identified, and the command does exactly what it says. The reasoning is sound and the outcome is catastrophic, which is why treating this as a model-behaviour problem leads to the wrong fixes.
Which commands are the dangerous ones?
The destructive-by-design members of every migration toolkit. In the Prisma ecosystem: migrate reset, which drops and recreates the database; migrate diff when its output is applied rather than inspected; and db push --accept-data-loss, whose flag name reads like a formality when you are moving quickly. In SQL directly: DROP, TRUNCATE, and DELETE without a WHERE clause. Other ecosystems have exact equivalents - rails db:reset, alembic downgrade base, django flush. None of these are exotic or misused; they are the recommended tools for schema reconciliation, which is precisely why an agent selects them and why a reviewer skimming a plausible debugging sequence does not stop.
Can't we just tell the agent not to touch production?
It helps and it is not sufficient. Instructions in a system prompt or a CLAUDE.md are context the model weighs, not a rule it is bound by - they usually hold, and usually is not a security property. Approval prompts fail for a different reason: an agent in an autonomous mode issues many commands, humans stop reading carefully around the twentieth, and that is exactly when the destructive one arrives. And the dangerous command frequently does not look dangerous in context, because migrate diff reads like an inspection verb and the destructive parameter is the least visually prominent part of the line. Controls that depend on a human reading carefully at the wrong moment are not controls.
What is the single most effective control?
A read-only database role for agent sessions by default, with write access granted explicitly and temporarily for a specific task. It dominates every other control because it is the only one that does not require correctly anticipating which command will be dangerous. A deny-list is valuable and you should have one, but it enumerates known-bad and there is always another way to lose data. A role without DDL or DELETE rights makes the whole class unreachable regardless of what the agent reasons its way into. The friction is genuinely small: elevate deliberately, then drop back.
Why do production credentials end up in the agent's hands?
Environment inheritance. Agent shells and MCP servers launched over stdio inherit environment variables from the user's shell profile, so a production DATABASE_URL exported in .zshrc for convenience is a production database available to every agent and every MCP server on that machine. Nobody granted that deliberately; it is a side effect of how process environments work. The fix is to keep production connection strings out of anything an agent shell inherits - separate profiles, separate shells, secret managers fetched per task rather than exported globally - and to inventory where those credentials currently sit before an agent finds them for you.
Where should a deny-list be enforced?
In a hook, below the model, where a prompt cannot negotiate with it. Where an agent exposes a control point such as the PreToolUse event in Claude Code, each tool call can be evaluated against policy and returned allow, deny or log before it executes, so a command matching migrate reset, --accept-data-loss, DROP or TRUNCATE is denied deterministically and the denial is recorded. This is the same conclusion AWS reached for CVE-2026-10591, where they moved sensitive files onto a protected-paths list requiring explicit approval and enforced it in the most autonomous mode as well as the supervised one, specifically so the model could not bypass the check by changing its own mode. Their newer Kiro Crew ships denied-by-default commands and sensitive-path blocking as day-one properties, which suggests the industry is converging on this placement.
Is this really an AI problem?
Not fundamentally. An agent holding write credentials on production is a process holding write credentials on production, and the controls for that have been well understood for decades: least privilege, credential separation by environment, deterministic guardrails on destructive operations, and tested point-in-time recovery. What agents change is the rate and the review model - they issue far more commands than a human, at a speed that defeats per-command approval, with reasoning that is usually correct and occasionally catastrophic. So the response is not novel AI controls but applying existing ones to a new kind of principal, and accepting that the principal's judgement cannot be the boundary.




