# Write a Blast-Radius File Before Your First AI Patch

> Source: <https://dev.to/gitgo_5662/write-a-blast-radius-file-before-your-first-ai-patch-ho5>
> Published: 2026-09-03 15:58:12+00:00

Your first AI patch should fail closed today. Do not ship a feature on day one. Prove one target file can revert cleanly now.

You joined a messy repo this morning. The assistant wants a wide rewrite. Your job is a tiny reversible cut only.

This drill gives you a blast-radius file first. You fill it before any model writes code. Then a short script checks the revert path.

Cheap code is not cheap to unwind. One extra import can touch auth. One extra migration can lock deploys.

You will not know the architecture yet. You also should not pretend otherwise. A blast-radius file makes unknowns explicit fast.

If the file cannot name a revert, stop. You do not prompt for more code. You shrink the change until revert is boring.

You will add two artifacts on your branch. Keep both files in the first PR.

`blast_radius.py`

— the contract for this change.`scripts/check_blast_radius.py`

— the fail-closed proof.The contract is the source of truth. The checker is the only merge gate. No green checker means no review yet.

Pick one production file you can read. Do not pick a whole folder. Do not pick generated vendor code.

```
git ls-files '*.py' '*.ts' '*.go' | head -n 40
TARGET=src/billing/invoice.py
wc -l "$TARGET"
git log -n 5 --oneline -- "$TARGET"
```

Read the last five commits on that file. Write two plain sentences in notes. Pick a smaller file if you cannot yet.

You now have a hard fence. Everything outside that fence is forbidden. Your assistant may not cross it.

Create `blast_radius.py`

at the repo root. Keep the dict small. Fill every field with your own hands.

```
# blast_radius.py
# Day-one contract. Humans edit this. Models do not.

BLAST = {
    "change_id": "day-one-001",
    "intent": "Add a fail-closed guard on invoice totals.",
    "target_files": [
        "src/billing/invoice.py",
        "tests/billing/test_invoice_flag_off.py",
        "blast_radius.py",
        "scripts/check_blast_radius.py",
    ],
    "forbidden_globs": [
        "src/auth/**",
        "**/migrations/**",
        "package-lock.json",
        "go.sum",
        "poetry.lock",
    ],
    "feature_flag": {
        "name": "INVOICE_GUARD_V1",
        "default": "off",
        "missing_means": "old_path",
    },
    "revert": {
        "strategy": "git_revert",
        "notes": "Flag off restores the prior totals path.",
    },
    "tests": [
        "pytest tests/billing/test_invoice_flag_off.py -q",
    ],
    "max_diff_lines": 80,
    "max_files": 4,
    "base_ref": "origin/main",
}
```

`missing_means: old_path`

is the fail-closed rule. A missing flag must not enable new behavior. That single line is the drill.

Do not ask a model for the feature yet. Write the test that proves the old path.

``` python
# tests/billing/test_invoice_flag_off.py
import os
from billing.invoice import compute_total

def test_missing_flag_uses_legacy_total(monkeypatch):
    monkeypatch.delenv("INVOICE_GUARD_V1", raising=False)
    assert "INVOICE_GUARD_V1" not in os.environ
    assert compute_total([100, 20], tax=0.1) == 120.0

def test_flag_off_uses_legacy_total(monkeypatch):
    monkeypatch.setenv("INVOICE_GUARD_V1", "off")
    assert compute_total([100, 20], tax=0.1) == 120.0
```

Run that file once before any patch. Watch it fail for a real reason. Put the exact command in `BLAST["tests"]`

.

If the test cannot run locally, stop here. Fix the harness before any AI edit. A junior without a test command has no proof.

Now you may use an assistant carefully. Feed it one file, not the tree. Paste `BLAST`

as the hard constraint.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

MonkeyCode provides free model access and a free server option. Use that loop to draft a patch against `invoice.py`

only. You still type every revert field yourself.

Keep the prompt short and strict.

```
Read blast_radius.py.
Edit only BLAST["target_files"].
Do not touch BLAST["forbidden_globs"].
Honor feature_flag default = off.
A missing flag must use the old path.
Keep the diff under max_diff_lines.
Return a unified diff, nothing else.
```

Reject any answer that adds extra files. Reject lockfile churn without discussion. Reject "while we are here" cleanups on sight.

Save this as `scripts/check_blast_radius.py`

. Run it with Python 3.

``` bash
#!/usr/bin/env python3
"""Fail the PR when the diff escapes the blast radius."""

from __future__ import annotations

import fnmatch
import subprocess
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from blast_radius import BLAST  # noqa: E402

def git(*args: str) -> str:
    return subprocess.check_output(["git", *args], cwd=ROOT, text=True)

def main() -> int:
    base_ref = BLAST["base_ref"]
    try:
        base = git("merge-base", "HEAD", base_ref).strip()
    except subprocess.CalledProcessError:
        print(f"cannot resolve merge-base with {base_ref}", file=sys.stderr)
        return 2

    names = [n for n in git("diff", "--name-only", base).splitlines() if n]
    allowed = set(BLAST["target_files"])
    forbidden = BLAST["forbidden_globs"]

    for name in names:
        for pat in forbidden:
            if fnmatch.fnmatch(name, pat):
                print(f"forbidden path in diff: {name}")
                return 1
        if name not in allowed:
            print(f"file outside blast radius: {name}")
            return 1

    if len(names) > BLAST["max_files"]:
        print(f"too many files: {len(names)}")
        return 1

    changed = 0
    for row in git("diff", "--numstat", base).splitlines():
        if not row.strip():
            continue
        added, deleted, name = row.split("\t", 2)
        if added == "-" or deleted == "-":
            print(f"binary file not allowed: {name}")
            return 1
        changed += int(added) + int(deleted)

    if changed > BLAST["max_diff_lines"]:
        print(f"diff too large: {changed} lines")
        return 1

    flag = BLAST["feature_flag"]
    if flag.get("default") != "off":
        print("feature flag must default off")
        return 1
    if flag.get("missing_means") != "old_path":
        print("missing flag must mean old_path")
        return 1

    print("blast-radius contract: ok")
    for cmd in BLAST["tests"]:
        print(f"+ {cmd}")
        subprocess.check_call(cmd, shell=True, cwd=ROOT)
    print("fail-closed checks passed")
    return 0

if __name__ == "__main__":
    raise SystemExit(main())
python3 scripts/check_blast_radius.py
```

The script is strict on purpose. It should fail your first attempt. That failure is the actual lesson here.

Do this on a throwaway branch today. Do not wait for production traffic later.

```
git checkout -b drill/day-one-guard
git add blast_radius.py scripts/check_blast_radius.py \
        src/billing/invoice.py tests/billing/test_invoice_flag_off.py
git commit -m "feat: invoice guard behind fail-closed flag"

python3 scripts/check_blast_radius.py

SHA=$(git rev-parse HEAD)
git revert --no-edit "$SHA"
INVOICE_GUARD_V1=off pytest tests/billing/test_invoice_flag_off.py -q
git log -n 3 --oneline
```

You must see the old test pass after revert. If it fails, the flag is not fail-closed. Fix that before you open any PR.

Reset the rehearsal when the proof is green.

```
git checkout -B drill/day-one-guard "$SHA"
```

Never force-push this proof to main. Keep the revert commit on the drill branch. Copy only the lesson into your real PR.

Your PR body should quote the blast-radius fields. Reviewers need the fence, not a long story.

```
## Blast radius
- Files: src/billing/invoice.py
- Flag: INVOICE_GUARD_V1 default off
- Missing flag: legacy totals
- Revert: git revert of this SHA

## Proof
- python3 scripts/check_blast_radius.py
- pytest tests/billing/test_invoice_flag_off.py -q
```

Ask the reviewer one question only today. Do not ask them to love the design.

Use this table when the assistant argues for more files.

| Model request | You do | Why it fails closed |
|---|---|---|
| Touch auth "just in case" | Refuse | Forbidden path |
| Add a migration | Refuse | Not reversible today |
| Rename a package | Refuse | Diff too wide |
| Refresh a lockfile | Refuse | Hidden blast radius |
| Default the flag on | Refuse | Not fail-closed |
| Edit one function plus tests | Allow | Inside the fence |
| Split work into two PRs | Allow | Shrinks revert |

Print the table beside your editor. Point at it when the model rambles on. Your job is the fence, not speed.

The checker says it cannot resolve merge-base. Fetch main, then retry the script once.

```
git fetch origin main
git rev-parse origin/main
python3 scripts/check_blast_radius.py
```

The checker says a file sits outside blast radius. You added a helper file by accident. Either shrink the patch or update `target_files`

with intent.

The old test still passes with the flag on. Your new path is not isolated yet. Put the new logic behind the env read first.

``` python
import os

# Proposal only. Unexecuted sample for a fail-closed read.
def compute_total(items, tax):
    flag = os.getenv("INVOICE_GUARD_V1", "off")
    if flag != "on":
        return sum(items) * (1 + tax)
    return guarded_total(items, tax)
```

Treat that snippet as unexecuted sample code only. Wire it to your real totals function after tests exist.

This drill will not teach system architecture. It will not catch semantic money bugs. It only bounds files, flags, and revert commands.

The script trusts `origin/main`

as merge base. Forks with `origin/master`

must change `base_ref`

now. You need git, Python 3, and pytest on PATH.

A comment is not a feature flag. An env var is a minimum bar. If your team has a flag service, use that name instead.

Free model access does not replace human review. A free server does not own production. You still run the tests locally.

Do not use this as a senior design review. Do not use it during live incident response. Do not use it to rubber-stamp generated refactors.

Skip it if you cannot run tests locally. Skip it if revert needs a force-push. Skip it if the change is a data migration.

Security patches need a different fence. Secret rotation is not a day-one AI drill. Ask a teammate before those changes.

You leave with a tiny, reversible PR. You also leave with a revert you already ran. That is enough work for day one.

Tomorrow you may widen the fence one file. You still start from `blast_radius.py`

. Cheap code stays cheap only when revert is boring.
