dbplex blog · Pattern

When TEST_DATABASE_URL points at production

Four wipes that started with a test, a shadow database or a cached config.

In short. In four of the database wipes we collected, nobody pointed the agent at production on purpose. A TEST_DATABASE_URL in .env held the production Neon URL. A Prisma shadow database URL held the production Supabase URL. A cached Laravel config still had production settings when the tests ran. A test the agent wrote ran against the production database on Render. In each case a command built to empty a database (test setup, a shadow reset, migrate:fresh) did exactly that, to the wrong one. The fix is to make the test target unable to reach production, by giving it different credentials and refusing to run anywhere but localhost.

Four wipes that began with a test

GPT-5.6 Sol and a test URL that was production

July 2026 · Codex · Postgres on Neon · Confirmed

The title of incident 1672 in the AI Incident Database
The AI Incident Database logged Bruno Lemos's case as incident 1672.

Bruno Lemos asked the model to generate seed data for local testing. It did, ran the end-to-end suite, then began a cleanup nobody asked for and ran TRUNCATE TABLE users CASCADE. His own postmortem, as relayed by paddo.dev, found the cause in the repository: .env set TEST_DATABASE_URL to the production database, and the tests ran destructive setup before each run. The variable name said test. The value said production.

Claude Opus 5 and a shadow database that was production

July 2026 · Claude Code · Supabase · Reported

During an autonomous schema repair, the agent ran prisma migrate diff with --shadow-database-url set to the direct production connection string. Prisma uses the shadow database as scratch space and resets it by design, so all 22 production tables were emptied, and replaying a stale migrations folder left two of them missing entirely. The command looked harmless. The danger was in its target.

Grok 4.6 and a cached production config

August 2026 · Cursor · MariaDB · Reported

Cursor forum thread, Grok 4.6 Agent wipes database, August 2026
The thread on Cursor's forum, with a reply from Cursor staff.

The agent ran php artisan test. A test using RefreshDatabase ran migrate:fresh, which drops every table. Laravel's config was cached with production settings, and a cached config ignores the test values in phpunit.xml and .env.testing, so the drop hit the live MariaDB. The host kept no binary log, so there was no point-in-time recovery. Cursor staff advised restoring from backups, using allowlist run mode and removing DROP rights from the app's database user.

Claude Code and a test against Render

October 2025 · Claude Code · Postgres on Render · Reported

GitHub issue 9966, Claude destroys production database, closed as not planned
The GitHub issue, closed as not planned.

Fixing a bug in team management, the agent wrote a test file that called sequelize.sync() and ran it. The environment pointed at the production database, and its data was wiped. The agent's own message, quoted in the issue: "I ran the test against YOUR PRODUCTION DATABASE". A commenter asked the question that matters: why did the agent have production access at all?

Why this keeps happening

Test tooling is built to destroy. Test setup truncates tables, RefreshDatabase drops them, a shadow database is reset on every diff, sync can recreate a schema. That is fine on a database made for it, and it is why the target matters more than the command.

The target is usually decided by a string in a file the agent can read and edit: .env, a cached config, a command-line flag. A human copying a production URL into the wrong variable once is enough, and the agent inherits the mistake every time it runs the tests.

How to make it impossible

Give tests credentials production does not accept. A test user and password that only exist on your local or disposable database fail at login if they ever point at production. Separate names in .env do not do this; separate credentials do.

Refuse to run anywhere but localhost. One check in the test setup turns a silent wipe into a loud failure:

// test/setup.ts
const url = new URL(process.env.TEST_DATABASE_URL ?? "");
if (!["localhost", "127.0.0.1"].includes(url.hostname)) {
  throw new Error(`Refusing to run tests against ${url.hostname}`);
}

Give the shadow database its own instance. Point Prisma's shadow database at a local throwaway database, and never at a URL that holds production credentials.

Clear cached config before tests. In Laravel, run php artisan config:clear before the suite, and do not cache config on a development machine at all.

Keep the production URL out of the repository the agent works in. If it is not in .env, it cannot end up in the wrong variable. Our guide to giving an agent a database covers the rest.

Where dbplex fits

Tests need real-looking data, which is how production URLs end up in test settings. dbplex gives you that data on localhost instead: it copies PostgreSQL, Supabase, MySQL and MariaDB and more to your Mac and hands you a local URL for TEST_DATABASE_URL. On every engine but Supabase you can save a state before the suite and restore it after, so destructive setup costs nothing. The agent gets the same copy over MCP and never sees the production credentials.

The Query screen of dbplex running SQL on a local copy of a database
A query on a local copy in dbplex. Your agent gets the same copy over MCP.

Download for Mac7 days free, no card and no account.

Sources

Sources: Bruno Lemos on X, paddo.dev, The Register, AI Incident Database 1672, AI Incident Database 1676, Tokenstead's reconstruction, the Cursor forum thread and GitHub issue 9966.