August 10, 2026·7 min read

Dry-run by default

The one rule that lets me run an autonomous system without lying awake about it: nothing writes to the outside world until I say so — and everything it does, it can undo.

By Andrew Pyle

The scary part of building a command center that acts on its own isn't the acting. It's the writing — the moment a script stops reading state and starts changing it. A redirect goes live. A page gets de-indexed. A config reloads on a production box. Those are the operations that turn a bad assumption into a bad afternoon. So I made a rule, and I made it the default rather than the exception: anything that writes to an outside system is a proposal first.

It's the sentence I put on my own site, and it's the load-bearing one: "Anything that writes to an outside system is dry-run by default. It shows me what it would do; it only acts when I say so." This is the mechanics of how I actually enforce that.

01The default

The default is "show me"

Every tool in the system that mutates external state has the same shape, and the shape starts from a deliberate inversion of the usual default. Run the command with no flags and it does not touch anything. It reads the current state, computes the change it intends to make, and prints that intention as a diff. Nothing is written. Nothing is irreversible. You have to opt in to action.

# no flag → dry-run. reads, computes, prints. writes NOTHING.
manage.py apply_seo_consolidation

# it tells you exactly what it WOULD do:
DRY-RUN — would set redirect_to on N cannibalizing pages:
  best-ai-tools        → best-free-ai
  ai-tools-list        → best-free-ai
  all-ai-tools         → best-free-ai
(pass --apply to write these)

The value here is boring and enormous: I get to read the change before it exists. Most production mistakes I've made in my life were confident actions taken on a wrong mental model. A dry-run collapses the gap between what I think a command will do and what it actually will do, at the only moment that gap is free to fix — before anything happened.

The command that does this on my own site is real, and its restraint has a subtlety I've come to like. `apply_seo_consolidation` reads the cannibalizing cluster, works out which page should survive, and prints the redirects it would set. Even when I pass `--apply`, all it writes is a `redirect_to` field on each losing page in the database — the 301 itself does not fire until that database change syncs to production. So there are two safety gaps, not one: the dry-run before I write anything, and the sync boundary before a written redirect becomes a live redirect a crawler can actually follow.

02Acting is explicit

Acting is an explicit verb

Action is never implicit. To actually write, you pass --apply. That single required flag is a small piece of friction placed exactly where friction belongs — between "I think this is right" and "this is now live in production." It's the difference between a command that can change the world and a command that just did.

The safest destructive command is one you can't run by accident. Making "act" a flag, not a default, means fat-fingering the command name does nothing but print a plan.

And when it does write, it doesn't just write. Before it changes anything, it records the prior state — the exact values it's about to overwrite — to a backup it controls. Applying and snapshotting are one atomic habit, not two things I have to remember to do in the right order.

The command also refuses to guess what I meant. Pass both --apply and --revert together and it stops with "Use either --apply or --revert, not both" rather than quietly picking one. Ambiguity about a production write is treated as a reason to do nothing at all. That's the correct bias for a tool I might invoke half-asleep, and the essential one for a tool I might hand to an agent to run unattended while I'm not watching.

03One shape

One shape, every tool

None of this is special to one command. Every tool I let near production wears the same three verbs, whichever corner of the system it touches. `apply_seo_phase0` sets `noindex` on templated, no-search-intent pages — reversible, because noindex only emits a `<meta robots noindex,follow>` tag and clears as cleanly as it sets. `apply_seo_consolidation` 301s a cannibalizing cluster into its survivor. `apply_seo_deepen` rewrites a post's body from a fixture. Three completely different operations — de-index, redirect, rewrite — and all three are dry-run by default, act on --apply, and undo on --revert.

The uniformity is the point. Because the shape never changes, I don't relearn the safety per tool, and neither does an agent running one. A command I've never seen before still answers the same two questions the instant I read its help: what would this do, and how do I take it back. That predictability is a form of reversibility of its own — I'm never surprised by the interface at the exact moment I'm about to change production.

04The undo

Everything has an undo

Because every apply writes a backup, every apply has an inverse. Pass --revert and the command restores the pre-change state from the snapshot it took. This isn't a manual "oh no" recovery written after the fact under pressure — it's designed in from the first line, and it's tested the same as the forward path.

Dry-run

Read state, compute the intended change, print the diff. No writes. This is the default with no flags.

Apply

--apply writes the change and, in the same step, records a backup of exactly what it overwrote.

Revert

--revert restores the pre-change state from that backup. The undo is part of the tool, not a hope.

There's a second property I lean on constantly: these commands are idempotent. Running an apply twice doesn't double the effect — the second run sees the change already in place and does nothing new. That means a re-run after an interrupted job is safe, and "did this already run?" stops being a question I have to answer nervously from memory.

05The backup

The backup knows what it did

The revert isn't a generic "put the world back." Each backup records exactly enough to reverse the one operation it guarded. Before `apply_seo_deepen` overwrites a post's body, it writes the original out to `backend/blog/data/deepen/<slug>.pre-deepen.json`. And it writes that snapshot once, never overwriting an existing one — so re-applying a later tweak can't quietly clobber the true original sitting underneath it. The first backup is the one that matters, and the tool protects it from itself.

The subtler case is a command that might either create or overwrite, because the undo has to do opposite things in the two cases. `create_writing_post` can add a brand-new essay or update one that already exists. So its backup carries a small flag, `__created_by_command`. On --revert, the tool deletes the post if it was the thing that created it, or restores the prior field values if it merely overwrote a post that was already there. The undo does the right thing without me having to remember, later and under pressure, which case I was in when I ran it.

This is the difference between reversibility as a feature and reversibility as an afterthought. An undo bolted on later tends to assume the happy path — it puts back what it thinks was there. An undo written into the forward path knows precisely what it changed, because it wrote down the truth a half-second before it touched anything. The backup isn't a copy of the state; it's a record of the exact edit, and that's what makes the reversal exact instead of approximate.

06The whole game

Why this is the whole game for autonomy

People assume the hard part of an autonomous system is making the agent smart. In my experience the hard part is making its actions safe enough that you'll actually let it act. A brilliant agent I can't trust with a production write is a demo. A modest agent whose every external write is previewable and reversible is a system I can leave running.

The dig-out from my own Google demotion was done entirely this way. I chose noindex over deletion for the worst pages precisely because noindex is reversible: a demotion recovery is a hypothesis, and I wanted the undo button if a pruned page turned out to matter. The consolidation redirects, the content rewrites — all of them dry-run first, applied on an explicit flag, backed up before they touched a row. The strategy got the headlines. This pattern is what made it safe to execute.

Dry-run-by-default is a small idea that quietly sets the ceiling on how much I can automate. It's not glamorous — a command whose proudest feature is that it usually does nothing. But it's the reason I can hand real work to a system and walk away: every irreversible-looking action is, underneath, a proposal I approved and a change I can take back. It's the same instinct as putting a deliberate human gate in front of the one step a deploy can't undo — let the machine run freely everywhere the work is reversible, and make the rare one-way door ask first. The autonomy people find impressive is downstream of the caution they never see.