The Bulk Script That Learned to Default to Dry Run
How a missing dry-run default caused an accidental email incident.
· 2 min read

# The Bulk Script That Learned to Default to Dry Run
A migration project I audited had already had its incident by the time I arrived: an import script, run to backfill some records, had accidentally sent live transactional emails to real customers who had no idea a migration was happening. Nobody had intended to email anyone. The script just did what scripts do when nobody has told them not to — it ran every code path, including the one that sends a welcome email on account creation, because from the script's point of view these were new accounts.
## The single missing default that caused it
The retry and backfill tooling that triggered the incident had no distinction between "simulate this and show me what would happen" and "actually do it." It had one mode, and that mode did everything, including side effects nobody had thought to gate. This is an extremely common shape for internal tooling, because the dry-run/apply distinction feels like ceremony when you're writing the first version of a script for a one-time job — until the one-time job runs twice, or runs against a bigger dataset than the last test, or a colleague runs it without the context you had in your head when you wrote it.
## What the actual fix looked like
The response wasn't a single patch — it was a small set of structural guardrails, each closing a different way the same failure could recur:
A transactional-email kill switch, so any code path could be prevented from sending regardless of which script triggered it — a backstop independent of getting every call site right.
An explicit import flag that the identity system's webhook handler checked, so account-creation events originating from a bulk import were distinguishable from a real user signing up, and could skip the welcome email specifically.
Retry and backfill tooling rewritten to default to a dry run, requiring an explicit flag to apply changes for real — inverting the default so that doing nothing was the safe path, not the fast one.
A written preflight checklist for anyone running a bulk script against production, turning what had been tribal knowledge in one person's head into something a second person could follow correctly on their first try.
## Why the checklist matters as much as the code
None of these fixes make a bulk script safe on their own. The kill switch only helps if someone remembers it exists during an incident. The dry-run default only helps if the person running the script doesn't reflexively add the apply flag out of habit. The webhook flag only helps if every new import path checks it. What actually makes the system safer is that these layers are independent — a mistake at any one of them still gets caught by another — and that the checklist makes the safe sequence the path of least resistance instead of the thing you have to remember under time pressure during a live cutover.
When I evaluate bulk-operation tooling now, I don't ask whether it has guardrails. I ask how many independent ones it has, because a single safety mechanism is a single point of failure with better branding, and the incident that reveals that is never a good time to find out.
More in Features
The Second Vendor Nobody Was Actually Calling
Adaeze Okorie
Unpushed isn't the same as safe: auditing git history for leaked secrets
Marcus Delacroix-Huang
It passed CI, then silently failed to apply in production
Tobias Wrenfeld


