The short answer
Let the agent run makemigrations, showmigrations, sqlmigrate and migrate --plan. Review the SQL yourself, then run migrate against anything that isn’t your laptop. Never let it run flush, migrate <app> zero or reset_db with a real database configured, and don’t give it manage.py shell or dbshell against production at all, because ORM code run there is invisible to any command check.
Deedglass
A database change, checked and put back
-
1The agent’s change
Change order 42
UPDATE orders … WHERE id = 42
The agent: “1 row updated” -
2What the database really did
301 rows
A hidden trigger changed 300 more
Doesn’t match -
3Put back
301 rows restored
Signed off by a person
Matches
The three commands that destroy data
Django’s own commands are explicit about what they do:
flushremoves all data from the database. The table of applied migrations is not cleared, so Django still thinks the schema is current.1 In interactive mode it asks you to typeyesand warns that it will irreversibly destroy all data.2migrate <app> zeroreverts every applied migration for that app, which drops its tables. Dependent migrations in other apps are unapplied too, whatever app label you gave.1reset_db(from django-extensions, not Django itself) drops the database and creates it again, empty.3
The one flag that makes all of this quiet is --noinput, which suppresses all user prompts.1 An agent running non-interactively will find it, because an unanswered prompt looks to it like a hang.
A workflow: the agent writes, you apply
The split that works is simple: the agent writes migrations and shows you what they’ll do; a person applies them.
1. The agent writes the migration.
Code Commands typed in a terminal. The text around it says what happens.
python manage.py makemigrations billing
2. The agent shows the plan. migrate --plan lists the operations a migrate would perform, without performing them:1
Code Commands typed in a terminal. The text around it says what happens.
python manage.py migrate --plan
python manage.py showmigrations billing
3. The agent prints the SQL. sqlmigrate prints the SQL for a named migration, and --backwards prints the SQL to unapply it. It needs a database connection to resolve constraint names, so run it against a copy of the database you’ll apply to.1
Code Commands typed in a terminal. The text around it says what happens.
python manage.py sqlmigrate billing 0004
python manage.py sqlmigrate billing 0004 --backwards
4. You read it. This is the step that catches the problems. Look for:
| In the SQL | What it means | Ask |
|---|---|---|
DROP TABLE |
A model was deleted or renamed without RenameModel |
Was the data moved first? |
ALTER TABLE … DROP COLUMN |
A field was removed or renamed as remove-and-add | Should it be RenameField? |
ALTER COLUMN … TYPE |
A field type changed | Will existing values convert? Will it lock a big table? |
ALTER COLUMN … SET NOT NULL with no default |
A required field on existing rows | Does every row have a value? |
DELETE or UPDATE inside RunSQL / RunPython |
A data migration | How many rows, and is there a reverse? |
5. You run migrate. With your credentials, against staging first.
This costs a few minutes per migration and removes the agent from the one step that can’t be taken back.
Check migrations in CI
Two --check flags turn the workflow into a gate:
Code Commands typed in a terminal. The text around it says what happens.
# fails if models changed but no migration was written
python manage.py makemigrations --check
# fails if migrations exist that haven't been applied
python manage.py migrate --check
makemigrations --check exits non-zero when model changes have no migration, and implies --dry-run; migrate --check exits non-zero when unapplied migrations exist.1 Run the first on every pull request. Run the second after deploy, to confirm the database matches the code.
What a command check sees
A pre-command hook in the coding agent sees each manage.py line before it runs. We ran Deedglass’s shell guard from 0.13.0 against Django commands on 2026-10-27:3
| Command | Answer |
|---|---|
python manage.py flush --noinput |
asks |
python manage.py migrate billing zero |
asks |
python manage.py reset_db --noinput |
asks |
python manage.py migrate |
nothing |
python manage.py migrate billing 0003 |
nothing |
python manage.py sqlmigrate billing 0004 |
nothing |
python manage.py shell -c "Order.objects.all().delete()" |
nothing |
python manage.py dbshell -- -c "TRUNCATE orders" |
nothing |
Code What the screen or file shows, copied as is.
$ npx deedglass@0.13.0 guard shell --no-files --command 'python manage.py migrate billing zero'
Deedglass asks first: manage.py migrate billing zero unapplies every migration of that app, which drops its tables.
django-migrate-zero manage.py migrate billing zero unapplies every migration of that app, which drops its tables
The first three are what you’d hope for. --noinput doesn’t change the answer, because the guard reads the command, not the prompt.
migrate billing 0003 passes. Migrating back to a named migration is routine, but if 0004 created a table, going back to 0003 drops it. Treat backward migrations on shared databases as a person’s job.
npx deedglass protect installs this guard in Claude Code, Cursor, Codex CLI, Gemini CLI and Copilot CLI.3
The shell is the real gap
The last two rows in that table are the ones to worry about. manage.py shell -c runs Python, and dbshell hands arguments to the database client. The guard reads text; code an interpreter runs passes untouched.3 Order.objects.all().delete() empties a table as surely as TRUNCATE, and a text check can’t know that.
So the boundary has to be the database:
- A separate database role for the agent. Point a
DATABASESentry the agent uses at a role withSELECTonly, or withINSERTandUPDATEon named tables. Our post on safe Postgres access for AI agents has the grants. - No production settings module in the agent’s environment. If
DJANGO_SETTINGS_MODULEpoints at a local settings file with a local database,flushis harmless. - Backups you’ve restored. A dump you have never restored is a hope, not a backup.
And remember the hook’s own failure mode: if it can’t run, because of a timeout or a missing binary, the command goes ahead unchecked.3
For other frameworks, see the table of migration commands that delete data and Rails migrations with an agent. The broader pattern is in letting an agent write migrations safely.
To see what the guard says about a command, run npx deedglass guard shell --no-files --command 'python manage.py flush --noinput'. It never runs the command. To grade what an agent can reach on your own machine, run npx deedglass scan (what the grade means). To watch the checks without installing anything, open the playground.
Written with AI assistance and checked against our claims register.