An AI assistant that can run commands will run the command it believes is correct, at the speed of a command. The difference between a helpful session and an expensive one is whether the action it took can be taken back.

The idea

Sort every action by whether you can undo it, and give supervision to the ones you cannot. A code edit is reversible if you committed before you started, because git holds only the versions you saved. Work you never committed has nothing you can rely on behind it, and mid-session that is most of the tree, which is why commit before you delegate is what puts code edits in the safe column. A deleted production table is not reversible, unless a backup exists and you have restored from it before. Anything that leaves your machine and reaches other people, such as an email send or a payment capture, cannot be recalled at all. The useful test is reversibility: it tells you which actions to sit in front of. Plenty of dangerous-sounding commands are fine to let run, and a few dull ones are not.

code
  reversibility            examples                          supervision

  free to undo             edit a file, run tests            let it run
  (if you committed        read logs
   first)

  undoable with effort     force-push, drop a local table    read first,
  (backup, reflog,         revert a deploy                   then approve
   redeploy)               install a package  (its setup
                           scripts run on your machine,
                           outside the diff you review)

  cannot be undone         email to real users               human types it
                           delete production rows
                           a migration on live data
                           revoke a key other systems use

How it works

Five categories account for nearly all of the real damage.

  • Deleting data. rm -rf, DROP TABLE, DELETE without a WHERE clause, emptying a storage bucket. The command succeeds silently: no confirmation step, no recycle bin.
  • Touching production directly. A live server, the production database, the DNS records for your domain. A change here has an audience immediately.
  • Credentials. Reading, printing, or rotating a key. An assistant that reads .env to debug a connection has put your secrets into the session transcript, and possibly the vendor's logs.
  • Migrations on live data. A that drops a column removes the data in it. Running it against production is a one-way action even when the file itself is in git.
  • Sending email to real users. Nothing recalls a broadcast. A test that reaches the live list instead of a seeded one is the usual way this happens.

git push --force sits at the edge. It is recoverable through the on a machine that still has the old commits, and much harder if the only copy of that branch was the remote one.

There is a sixth case the reversibility test does not cover, because it assumes you chose the action. Your assistant reads whatever you point it at: a GitHub issue, a dependency's README, a web page, a row a stranger typed into your own app. Text in any of those can be phrased as an instruction, and an assistant working through it may act on that text rather than report it. You cannot review a decision you did not know was being made, which is why the permission list matters more than the supervision. An assistant that cannot delete, cannot reach production and cannot read .env is a far smaller problem on the day it reads something hostile.

What to do

  1. Replace blanket approval with a written rule list. In Claude Code, /permissions opens the allow and deny lists, and each entry is a tool name with an optional pattern: Bash(git status:*) and Read(src/**) on the allow side, Bash(rm:*) and Read(./.env*) on the deny side. Note the *: Read(./.env) is an exact path and leaves .env.local readable. Deny rules are pattern matches, so they stop the accident rather than a determined bypass.
  2. Give it a database that is not production. Make a second Supabase project, apply your migrations to it, and put that connection string in .env.local. The credential in your project folder, where the assistant works, is always the development one. Production credentials live in your host's environment settings. If you ever genuinely need one locally, put it in a file the deny rule covers rather than typing it into the terminal, where it lands in your shell history and in the session transcript.
  3. Ask for the command instead of the execution when a destructive step is genuinely needed: "Write the migration and the rollback, and show me both. Do not run either." You paste it yourself, against the development database first.
  4. Take a backup before the irreversible step, and prove it restores by loading it into a separate empty database, never over the live one. An untested backup is a belief. Backups covers how to make one.

Where it breaks

Supervision only works if you read what you approve. A prompt that asks for confirmation twenty times an hour trains you to press yes, which is how a wrong command gets approved by someone who was technically supervising. Narrow the allowlist until the prompts are rare enough to read. The other limit is scope: an assistant with a legitimate database connection can issue a destructive statement without running a shell command at all, so a shell deny list does not protect the data. What closes that gap is the connection string. Give the app a database user that can read and write rows but cannot drop tables, and keep the owner credentials for migrations you run by hand.