cd /news/developer-tools/write-a-blast-radius-file-before-you… · home topics developer-tools article
[ARTICLE · art-120507] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Write a Blast-Radius File Before Your First AI Patch

A developer at MonkeyCode outlines a fail-closed strategy for introducing AI-assisted code changes, emphasizing the creation of a blast-radius file before any patch. The approach requires a reversible change, a test proving the legacy path, and a checker script as the sole merge gate.

read7 min views6 publishedSep 3, 2026

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 = {
    "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.

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.

#!/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.

import os

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.

── more in #developer-tools 4 stories · sorted by recency
── more on @monkeycode 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/write-a-blast-radius…] indexed:0 read:7min 2026-09-03 ·