The useful line between what a model may draft and what a human must own is not "who typed it" — it is blast radius, meaning how expensive a wrong sentence is for the reader who acts on it. A wrong sentence that sends someone to rm -rf ./build costs one rebuild; a wrong sentence that sends someone to drop a production table costs a weekend. Everything below follows from that single cost difference, and it produces a checker you can run on every push instead of a policy page nobody reads.
So the split is: cheap-to-be-wrong blocks get drafted and skimmed; expensive blocks stay in human hands, must ship with a reachable undo step, and get re-verified on a schedule. This post covers the rule, the lint, the workflow, and where the lint quietly fails.
1. Classify by cost, not by section title
Most teams split docs by heading ("reference is generated, tutorials are hand-written"). That split does not survive a tutorial that tells readers to revoke a key.
| Signal in the block | Worst case if the sentence is wrong | Class | Who writes it |
|---|---|---|---|
| Motivation, "why this exists", prerequisites | Reader is confused, edits one line | Draftable | Model drafts, reviewer skims |
Read-only commands (curl GET, docker ps, SELECT) |
Bad output, retry | Draftable | Model drafts |
Destructive commands (DROP TABLE, terraform destroy, kubectl delete, git push --force) |
Data loss or outage | Human-owned | Human writes the command and the undo |
| Guarantees ("encrypted at rest", "zero-downtime", "atomic") | Security or contract misstatement | Human-owned | Human, linked to the primary source |
| Numeric limits (quota, timeout, max payload) | Wrong capacity plan | Human-owned unless emitted by code | Generator or human |
| Compatibility claims ("works on Node 18+") | Broken install | Human-owned unless a test matrix emits it | Human or CI |
Note what the model still does inside a human-owned block: it can draft the reason for the migration, the warning about downtime, and the ordering of steps. What it cannot own is the destructive token itself and the sentence that gets the reader back out. That is the whole editorial split in one sentence.
2. Two rules that make the class checkable
Rule A — undo reachability. Every destructive command must be followed within roughly 120 characters by a recovery affordance (restore, undo, revert, re-create, rollback, from backup) or a link to a recovery doc. No undo path, no merge.
Rule B — fact freshness. Every guarantee and every numeric claim carries a source URL and a checked: YYYY-MM-DD stamp; anything older than 180 days fails until a human re-opens the primary source. This is the cheap defense against last year's quota table surviving into this year's onboarding page.
Both rules are regex-checkable, which is the point. Imperfect, transparent, and fast enough to run on every commit.
3. The checker
# blast_radius.py — reference implementation, thresholds untuned
import re, sys, pathlib
DESTRUCTIVE = re.compile(
r"(rm\s+-rf|drop\s+(table|database)|truncate\s+table|terraform\s+destroy|"
r"kubectl\s+delete|git\s+push\s+--force|revoke\s+(the\s+)?(key|token)|delete\s+from)",
re.I,
)
UNDO = re.compile(r"(restore|undo|revert|re-?create|re-?enable|rollback|snapshot|from\s+backup)", re.I)
GUARANTEE = re.compile(r"\b(SLA|guarantee[sd]?|encrypted at rest|zero[- ]downtime|atomic(ally)?)\b", re.I)
NUMBER = re.compile(r"\b\d+\s?(ms|s|m|h|requests|rps|MB|GB|%|per\s+(day|minute|month))\b", re.I)
CHECKED = re.compile(r"checked:\s*(\d{4}-\d{2}-\d{2})")
def blocks(text):
for i, chunk in enumerate(re.split(r"\n\s*\n", text)):
yield i, chunk.strip()
def scan(path):
text = path.read_text(encoding="utf-8")
findings = []
for idx, block in blocks(text):
if not block:
continue
if DESTRUCTIVE.search(block) and not UNDO.search(block):
findings.append((path, idx, "HUMAN", "destructive command without undo path"))
if GUARANTEE.search(block) and not CHECKED.search(block):
findings.append((path, idx, "HUMAN", "guarantee without source + checked date"))
if NUMBER.search(block) and not CHECKED.search(block):
findings.append((path, idx, "HUMAN", "numeric claim without checked date"))
return findings
if __name__ == "__main__":
root = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else "docs")
all_findings = [f for p in sorted(root.rglob("*.md")) for f in scan(p)]
for path, idx, cls, why in all_findings:
print(f"{path}:block{idx} [{cls}] {why}")
sys.exit(1 if all_findings else 0)
Wire it into a pre-merge step so a failure is a review request rather than a build break:
python blast_radius.py docs/ \
&& echo "docs: pass" \
|| echo "docs: human review required (see block list above)"
The script is deliberately dumb: no AST, no model call, no config file to drift. If you need a per-repo lexicon, keep it in the file and review edits to it like code. I have not benchmarked the thresholds against a large documentation set, so treat the 120-character window and the 180-day expiry as starting points to tune, not as measured constants.
4. The workflow, in five steps
-
Freeze the fact inputs. Pull versions, defaults, and limits out of the source of truth — config schema,
--helpoutput, OpenAPI spec — into a small JSON file. The model reads numbers from that file; it never retypes them from memory. - Draft only the cheap blocks with a model. Narrative, motivation, prerequisites, read-only examples. This is exactly where free model access is enough, since nothing it writes here can destroy anything; I use MonkeyCode's free model access for this step and keep every flagged block outside the model entirely. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
- Run the checker before human review. It turns "please review this doc" into an enumerated list of human-owned items, which is the difference between a 40-minute read and a 10-minute checklist.
-
Route flagged blocks to a named human with a narrow job. Run the undo command for real in a scratch environment, confirm each guarantee against the vendor's primary docs page, then stamp
checked: YYYY-MM-DD. Nothing else is in scope. - Re-check on a schedule. A weekly cron job re-runs the lint and opens an issue for expired stamps. That job is small enough to sit on a free server instance rather than a paid runner, so the freshness check keeps running after everyone forgets the doc exists.
A synthetic fixture with two problems produces output like this:
docs/rotate-key.md:block4 [HUMAN] destructive command without undo path
docs/limits.md:block2 [HUMAN] numeric claim without checked date
The first line is the interesting one. It does not say "a model wrote this." It says: someone is about to revoke a live credential and the page never tells them how to recover.
5. Where the lint fails
-
False positives on teaching prose. A
DELETE FROMinside an article about transaction isolation gets flagged. Correct, but noisy; add an inline<!-- draftable -->escape and review those diffs by hand. - Lexicon gaps are the real failure mode. "Remove the old database" with no command and no keyword sails straight through. Non-English docs, PDFs, and screenshots are invisible to the checker.
- Stated recoverability is not actual recoverability. The undo regex passes "you can restore from backup" even when the backups were never configured. The tool measures the sentence, not the system; a human still has to confirm the backup exists once.
- It says nothing about accuracy. A confidently wrong explanation of a read-only command passes cleanly. Cheap blocks are cheap to fix precisely because nobody runs them as written.
6. Who should not use this
Skip it if your destructive steps are all mediated by a control plane that already forces its own confirmation, since the doc sentence then cannot cause the damage. Skip it if docs ship inside the product UI and the button, not the prose, does the deleting. Skip it for single-maintainer projects where the same person drafts and reviews everything, because the routing step adds nothing. And do not use a green lint as permission to auto-approve: the script sees tokens, not intent.
7. What changes in practice
The measurable shift is where reviewer attention goes: from "read 900 lines of prose" to "run three undo commands and verify two numbers against primary sources." Cheap blocks scale with model drafting; expensive blocks get a smaller, faster, more specific human review, and the freshness stamps keep the numbers from rotting silently.
A question worth ten minutes on your own repository: how many destructive commands in your docs have an undo path within one screen of the command? That count — not word coverage, not reading level — is the documentation risk that actually reaches production.
Top comments (0)