---
name: destructive-action-gate
description: Classifies operations as reversible, recoverable or irreversible and gates only the last kind, requiring a stated blast radius, a verified restore point and a rollback command before it runs. Ships deny rules and a pre-tool hook for remote database execution, force pushes, bulk deletes and infrastructure applies. Use when setting up guardrails or before any production operation.
license: MIT
compatibility: Claude Code and similar agent harnesses with hooks and permission settings. Applies to Postgres, D1, S3-compatible storage, git and Terraform.
metadata:
  category: security
  version: "1.0.0"
---

# Destructive Action Gate

Every team that has lost data has the same story afterwards: the command was familiar, the flag was
one character different from usual, and nothing asked. The instinct is then to confirm everything —
which produces a prompt on `ls`, teaches everyone to hit yes without reading, and leaves you worse
off than before.

**A gate that fires on everything gets switched off within a week.** The discipline is to fire rarely,
and to make it count when it does.

## Classify first

Sort every operation into three buckets. Only the third is gated.

- **Reversible** — undo is trivial and local. Editing a file, a local commit, adding a column, a
  feature branch. **Never gate these.**
- **Recoverable** — undo exists but costs something: a backup restore, a revert deploy, a reflog
  rescue. Worth logging and worth naming out loud. Not worth blocking.
- **Irreversible** — no undo, or an undo with a deadline you might miss. Dropping a table, deleting
  storage objects, force-pushing over someone else's work, deleting a secret, sending real messages,
  charging real money.

The line moves with configuration, and you have to check rather than assume. `DELETE FROM` is
recoverable with point-in-time restore and irreversible without it. Object deletion is recoverable on
a versioned bucket and permanent on an unversioned one. **Verify which you have before you decide
which bucket you are in.**

## The three things required before an irreversible action

Not a yes/no prompt — a yes/no prompt is answered reflexively. Three statements, in writing:

1. **Blast radius.** What exactly is affected, and how much. Not "some rows" — run the `SELECT COUNT(*)`
   with the same `WHERE` clause first and state the number. A count that surprises you is the single
   most common way this gets caught in time.
2. **A verified restore point.** Verified, not assumed. Name the backup, its timestamp, and the fact
   that you checked it exists. "Backups run nightly" is not a restore point; a bookmark id is.
3. **The rollback command**, written out, ready to paste. If you cannot write it, you do not have one,
   and that is the finding.

If any of the three cannot be produced, the action does not run. That is the whole gate.

## Wire it mechanically

Human discipline decays; make the harness carry it. Generate:

- **Deny and ask rules** in `.claude/settings.json` for the patterns that matter: `--remote` database
  execution, `DROP`/`TRUNCATE`, `rm -rf`, `git push --force` (allow `--force-with-lease`),
  `terraform apply` and `destroy`, bulk object deletion, secret deletion, `DELETE`/`UPDATE` without a
  `WHERE` clause.
- **A PreToolUse hook** that inspects the command and blocks with a message naming which of the three
  statements is missing, so the block teaches rather than merely obstructs.
- **A separate credential path for production.** The strongest control here is not a prompt — it is
  that the everyday credential cannot perform the destructive operation at all.

Match on the shape of the command, not an exact string. `wrangler d1 execute --remote` and
`wrangler d1 execute db --remote --file x.sql` must both match.

## Recovery, for the minute after it happens anyway

Write this down before you need it, because you will be reading it in a hurry:

- **Postgres** — point-in-time recovery to just before the statement, if WAL retention covers it.
  Check the retention window now, not then.
- **Cloudflare D1** — Time Travel restores to a bookmark or timestamp, typically within 30 days.
- **S3 and R2** — a versioned bucket keeps the previous version and a delete marker you can remove.
  An unversioned bucket keeps nothing.
- **git** — `git reflog` recovers a clobbered local branch for around 90 days. A force-pushed remote
  branch is recoverable only from someone's local clone, so ask the team before rewriting history.
- **Secrets** — usually unrecoverable. Rotation is the plan, not restoration.

The retention window is the deadline. Find the number for each system and record it.

## Judgement calls

- **Staging and production are different.** Gate production. Gating a scratch database trains people
  to ignore the gate, which costs you the one that matters.
- **Bulk operations deserve extra scrutiny even when reversible.** Restoring ten million rows is
  technically possible and practically a very long day.
- **Never widen a gate to get unblocked.** If a rule fires and the action is legitimate, satisfy the
  three statements; do not edit the rule. The edit outlives the moment.

## Verification gate

- [ ] Deny rules are in place and **tested against a deliberately dangerous command** — an untested
      gate is not a gate.
- [ ] The hook blocks and explains which statement is missing, rather than failing silently.
- [ ] Retention windows for every backing store are written down with real numbers.
- [ ] A restore has been rehearsed at least once against a non-production copy.
- [ ] Reversible operations are demonstrably not gated — confirm the everyday workflow is unaffected.
