A markdown file is a request. A hook that exits 1 is a rule.
This week someone asked how to stop coding agents from acting out of line. The easy case is "don't write the README." The hard case is "database migrations must be autogenerated, never handwritten." A sentence in AGENTS.md will not hold that line. A pre-commit hook will.
I shipped the pack as a same-day zip. The useful part is below. The full hook + CI + Cursor rule is the paid file.
Buy the pack: https://buy.stripe.com/7sYaEXcmI0obaWV4Q77ss0m ($24, Stripe Payment Link, no account needed to pay)
Soft vs hard
| Intent | Why markdown fails | Hard check |
|---|---|---|
| Do not write README / CHANGELOG | Models "helpfully" update docs | Deny-list path match |
Do not touch .env or lockfiles |
Models "finish the setup" | Deny-list path match |
| Migrations must be autogenerated | Models write SQL that drifts from the ORM | Require a generator header or fail |
| Do not add dependencies | Models grab a package to look busy | Fail if the manifest changed |
If you cannot write a check that exits 1, it is not a protocol. It is a vibe.
Drop-in AGENTS.md (free, use today)
# Agent protocol
You may edit files listed in the plan. You may add a test that proves the plan.
You may autogenerate a migration with the project generator, then stop.
You may not write README.md, CHANGELOG, docs/, or .env.
You may not handwrite a SQL or ORM migration.
You may not add a dependency.
You may not pass --no-verify.
If the plan needs a schema change and you cannot run the generator, stop. Do not write the SQL.
Copy that to AGENTS.md and CLAUDE.md. Soft. Necessary. Not enough.
The check that makes it hard
Put this at tools/check-protocol.py. The hook and the GitHub Action both call it.
import pathlib, re, sys
DENY = {"README.md", "CHANGELOG.md", ".env"}
MIG_RE = re.compile(r"(^|/)(alembic/versions|migrations|prisma/migrations|drizzle|supabase/migrations|db/migrate)/")
GEN = re.compile(r"(GENERATED-BY:|Auto-generated by|Generated by (Alembic|Prisma|Rails|Django|drizzle-kit))", re.I)
SQL = re.compile(r"\b(CREATE|ALTER|DROP)\s+(TABLE|INDEX|TYPE)\b", re.I)
def denied(p):
return pathlib.Path(p).name in DENY or p.startswith("docs/") or p.startswith("secrets/")
failed = []
for path in [ln.strip() for ln in sys.stdin if ln.strip()]:
if denied(path):
failed.append("DENY " + path)
continue
if MIG_RE.search(path):
body = pathlib.Path(path).read_text() if pathlib.Path(path).exists() else ""
if body and not GEN.search(body) and SQL.search(body):
failed.append("HANDWRITTEN MIGRATION " + path)
if failed:
sys.stderr.write("\n".join(failed) + "\n")
sys.exit(1)
Pre-commit (.git/hooks/pre-commit):
#!/bin/sh
set -e
git diff --cached --name-only --diff-filter=ACMR | python3 tools/check-protocol.py
That is the handwritten-migration case: the agent can still want to write ALTER TABLE. The commit dies anyway.
What is in the $24 zip
-
01-hard-vs-soft.md— the matrix, with the dependency and schema rows -
02-AGENTS.md— the drop-in protocol -
03-no-handwritten-migrations.mdc— Cursor always-on rule -
04-pre-commit-protocol.sh— installable hook -
05-protocol-check.yml— GitHub Action on PRs -
06-check-protocol.py— the checker with editable deny-list and migration globs
Pay here: https://buy.stripe.com/7sYaEXcmI0obaWV4Q77ss0m
After payment the Stripe receipt is the ticket. Email that receipt to rileycraig14@gmail.com and the zip goes out.
This is a file, not a comment, not a retainer, not GEO.
Top comments (0)