Django migrations and AI coding agents: which manage.py commands to allow

An agent that can run manage.py can run flush and migrate zero. Here is a workflow that lets it write migrations while a person reviews the SQL, and what a command check sees.

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 reads what Postgres actually committed, not what the agent reported. In our demo a hidden trigger turned 1 row into 301; a person countersigned the undo and all 301 rows came back.

The three commands that destroy data

Django’s own commands are explicit about what they do:

  • flush removes 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 type yes and warns that it will irreversibly destroy all data.2
  • migrate <app> zero reverts every applied migration for that app, which drops its tables. Dependent migrations in other apps are unapplied too, whatever app label you gave.1
  • reset_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:

  1. A separate database role for the agent. Point a DATABASES entry the agent uses at a role with SELECT only, or with INSERT and UPDATE on named tables. Our post on safe Postgres access for AI agents has the grants.
  2. No production settings module in the agent’s environment. If DJANGO_SETTINGS_MODULE points at a local settings file with a local database, flush is harmless.
  3. 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.

The takeaway

The guard asked first on manage.py flush, migrate <app> zero and reset_db, not on ORM deletes in manage.py shell; review SQL with sqlmigrate.

Read nextRails migrations and AI coding agents: db:migrate yes, db:reset no Try it: install in 7 s

Related on this site

More posts

Sources

  1. 1Django 5.2 docs: django-admin and manage.py · docs.djangoproject.com · checked 27 October 2026
  2. 2django/django: flush.py (stable/5.2.x) · github.com · checked 27 October 2026
  3. 3Deedglass docs: The shell guard · docs.deedglass.com · checked 27 October 2026

Published 27 October 2026, sources last checked 27 October 2026. If something here is out of date, write to hello@deedglass.com.

Try it on your own setup

npx deedglass scan

Grades only your own setup. Nothing is uploaded.

Pricing