Index
Feed
Written by nadia.codes · Apr 30, 2026
Never let an agent's migration touch a database you care about on the first run. A disposable shadow database catches destructive DDL, broken backfills, and the classic 'rename via drop-and-create' before they reach even your dev data.
First rule: the agent's live connection (MCP or otherwise) cannot write. Create a role that can inspect schema and data but not modify it. Migrations happen through files + your runner, never through the live connection.
CREATE ROLE agent_ro LOGIN PASSWORD :'pw';
GRANT CONNECT ON DATABASE myapp_dev TO agent_ro;
GRANT USAGE ON SCHEMA public TO agent_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO agent_ro;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO agent_ro;The shadow DB is created from your current schema, gets the candidate migrations, and is destroyed. Make it one command so it actually gets used.
#!/usr/bin/env bash
# bin/shadow-migrate — run candidate migrations against a throwaway DB
set -euo pipefail
SHADOW=myapp_shadow
dropdb --if-exists "$SHADOW"
createdb "$SHADOW"
pg_dump --schema-only myapp_dev | psql -q "$SHADOW"
DATABASE_URL=postgres://localhost/$SHADOW python manage.py migrate
echo 'Shadow migration OK'! Common failures
Did this recipe work for you?
Sign in to add your report — every count here is backed by a named account.
The agent's definition of done is 'bin/shadow-migrate passes', not 'migrate passes'. Put that in your repo instructions so it's the loop the agent naturally runs.
bin/shadow-migrateDiff-level review: dump the migration to SQL and grep for the operations that lose data. Any hit means human review before the migration goes anywhere — agents notoriously implement column renames as drop+add.
python manage.py sqlmigrate app 0042 > /tmp/mig.sql
if grep -inE 'DROP (TABLE|COLUMN)|TRUNCATE|DELETE FROM|ALTER COLUMN .* TYPE' /tmp/mig.sql; then
echo 'Destructive DDL found — human review required.' >&2
exit 1
fiMigrate forward, then immediately back, on the shadow. Agents write plausible forward migrations and impossible reverse ones; this catches it while it's free.
DATABASE_URL=postgres://localhost/myapp_shadow python manage.py migrate app 0042
DATABASE_URL=postgres://localhost/myapp_shadow python manage.py migrate app 0041After shadow passes, apply to dev and confirm the resulting schema matches what shadow produced — a mismatch means environment drift you want to know about today.
python manage.py migrate
pg_dump --schema-only myapp_dev > /tmp/dev.sql
pg_dump --schema-only myapp_shadow > /tmp/shadow.sql
diff /tmp/shadow.sql /tmp/dev.sql && echo 'Schemas match'Related records
Strong evidence gets promoted into the record above.
Converted skeptic checking in: this is the first agent workflow I've adopted wholesale. The shadow DB idea isn't new (Prisma's had it for years) but pointing the AGENT'S definition of done at it is the actual insight.
Sign in to join the discussion, vote, and verify fixes.
Question — why schema-only for the shadow instead of a full copy of dev?
Speed, mostly: schema-only rebuilds in seconds so the agent can iterate. Full copies also tempt people into pointing the shadow at data they care about, which defeats the purpose. Sample data when you need it (see common failure #1), never the real thing.