The short answer
Let the agent write migrations and run bin/rails db:migrate and db:migrate:status against development. Keep db:reset, db:drop, db:schema:load, db:truncate_all and db:migrate VERSION=0 for people. Rails already refuses the destructive ones in production, but one environment variable switches that off, so add a check the agent can’t switch off from the same command line.
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 brake Rails already has
Active Record has a protected-environments check. In Rails’ own task file, db:drop, db:purge, db:truncate_all and db:schema:load each run check_protected_environments first; db:reset and db:setup call those tasks, so they’re covered too.2 The list of protected environments defaults to ["production"].4 Run one of those tasks there and you get:
Code What the screen or file shows, copied as is.
You are attempting to run a destructive action against your 'production' database.
If you are sure you want to continue, run the same command with the environment variable:
DISABLE_DATABASE_ENVIRONMENT_CHECK=1
That message is copied from ProtectedEnvironmentError in Rails’ source.3 There’s a second check alongside it: if the database was last used by a different environment than the one you’re running, Rails raises an environment-mismatch error and tells you to run bin/rails db:environment:set.3
db:migrate has no such check.2 That’s intended: forward migrations are how production gets its schema.
Why it isn’t enough for an agent
Read the error again from an agent’s point of view. It ran a command, the command failed, and the error says exactly how to make it succeed. Setting DISABLE_DATABASE_ENVIRONMENT_CHECK=1 skips the check entirely.2 A person reads that message and stops to think. An agent optimising for “the command worked” sets the variable.
The brake also only knows about the environment Rails thinks it’s in. If the agent’s shell has DATABASE_URL pointing at production and RAILS_ENV unset, the protection depends on what the database recorded about its last environment. Don’t build a safety plan on that.
Commands to allow, review and block
The Rails guides describe the routine commands: db:migrate:status shows each migration as up or down, db:rollback reverts the latest migration (STEP=3 the last three), and db:reset drops the database and sets it up again.1 Here’s how we’d sort them for an agent, with the answer Deedglass’s shell guard (0.13.0) gave each on 2026-10-27:6
| Command | Our advice | Guard answer |
|---|---|---|
bin/rails db:migrate |
allow (development); a person applies in production | nothing |
bin/rails db:migrate:status |
allow | nothing |
bin/rails db:rollback STEP=3 |
review: rolling back a create drops the table | nothing |
bin/rails db:migrate:redo STEP=3 |
review, same reason | nothing |
bin/rails db:reset |
block | asks |
DISABLE_DATABASE_ENVIRONMENT_CHECK=1 bin/rails db:reset |
block | asks |
RAILS_ENV=production bin/rails db:schema:load |
block | asks |
bin/rails db:migrate VERSION=0 |
block | asks |
bin/rails db:seed:replant |
block | asks |
bin/rails runner "User.delete_all" |
block outside development | nothing |
bin/rails dbconsole -p <<< "DROP TABLE users;" |
block outside development | nothing |
Code What the screen or file shows, copied as is.
$ npx deedglass@0.13.0 guard shell --no-files --command 'DISABLE_DATABASE_ENVIRONMENT_CHECK=1 bin/rails db:reset'
Deedglass asks first: rails db:reset drops the database and builds it again, so its data is gone.
rails-db-reset rails db:reset drops the database and builds it again, so its data is gone
The second row is the point of the whole post. The guard splits off VAR=value prefixes and judges the command itself, so the variable that disarms Rails’ check doesn’t disarm this one.6 The Rails rows in its catalog each carry a source in Rails’ guides, API docs or task file.6
db:seed:replant surprises people: the catalog files it with db:truncate_all and db:purge, because it empties every table in the database.6
npx deedglass protect installs the guard in Claude Code, Cursor, Codex CLI, Gemini CLI and Copilot CLI.6
Strong Migrations for the migrations themselves
The guard looks at which task runs. It doesn’t read the migration file. For that, the Strong Migrations gem detects potentially dangerous operations on PostgreSQL, MySQL and MariaDB, prevents them from running by default, and suggests a safer way. It counts an operation as dangerous if it blocks reads or writes for more than a few seconds once it has its lock, or has a good chance of causing application errors.5
It’s worth adding for agent-written migrations in particular, with one caveat: a step wrapped in safety_assured { ... } runs anyway.5 An agent that hits the error will learn to add the wrapper, just as it learns DISABLE_DATABASE_ENVIRONMENT_CHECK. Make safety_assured something your code review looks for:
Code Commands typed in a terminal. The text around it says what happens.
git diff origin/main -- db/migrate | grep -n "safety_assured"
A non-empty result on an agent’s pull request means a person should read that migration line by line.
Questions people ask
Does Rails stop db:reset in production?
By default, yes. db:drop, db:purge, db:truncate_all and db:schema:load check the protected environments first, and db:reset runs db:drop, so it refuses in production. Setting DISABLE_DATABASE_ENVIRONMENT_CHECK=1 skips the check.2
Is bin/rails db:migrate safe for an agent to run?
It has no protected-environment check, because forward migrations are how production gets its schema.2 It’s as safe as the migration it runs, so a person should read agent-written migrations before they reach a shared database.
Can an agent get past the protected-environment check?
Yes, with one environment variable, and the error message names it. A check outside Rails still saw the command: the shell guard asked first on DISABLE_DATABASE_ENVIRONMENT_CHECK=1 bin/rails db:reset.6
Where the text check stops
The last two rows of the table get no question, and they’re as destructive as db:reset. rails runner executes Ruby, and dbconsole passes SQL to the database client on standard input. A text check reads the command as written; code an interpreter runs passes untouched.6 If the hook can’t run at all, the command goes ahead unchecked.6
So close the gap at the database:
- A database role for the agent that can’t drop or truncate. Point a
database.ymlentry the agent uses at a role with only the grants it needs. Safe Postgres access for AI agents lists them. - No production credentials in the agent’s environment. Not in
.env, not inconfig/credentials, not in the shell profile it inherits. - A person runs
db:migratein production, after reading the migration.
For other frameworks, see the table of migration commands that delete data and Django migrations with an agent. The general pattern is in letting an agent write migrations safely.
To check a Rails command yourself, run npx deedglass guard shell --no-files --command 'bin/rails db:reset'. It reads the command and never runs it. 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.