I almost pointed a summarizer at real notes. The dry run looked calm and fully complete. Was non-empty markdown a real ship signal? Not in this house.
A free-model job can fake the engineering work. It returns a warm paragraph and exits zero. Who actually checks that the JSON fields exist?
I wanted a production permit, not another vibe list. The permit is one JSON file plus a checker. The model does not start until every gate has evidence.
Why a permit, not a README
READMEs never refuse a bad write command. Scripts can refuse, so I wrote a script. I needed fail-closed behavior on a laptop first.
I also needed the same check on a free server. Same permit. Same exit codes. Same evidence paths. Why copy the file instead of trusting SSH? Because the server does not know my laptop passed.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode matters here as a free-model lane and optional free server. I treat both as untrusted compute for canaries. A free lane is not a production blessing.
Do you trust a model because the server was free? Price is not a gate. Evidence is a gate. The permit has to travel with the job.
Time box, cost, and rollback
Give it forty-five minutes and zero dollars. Use a sandbox tree, never your real notes. If the checker still cannot refuse, abandon the approach.
Rollback is one permit file on disk. Delete permit.json and the job must refuse. If it still writes, your wrapper is lying. That lie is the first bug to fix.
I also keep a previous output hash nearby. New files look wrong? Copy the old tree back. No hash, no real-write lane. That is the rollback path I want.
Copy these files
Keep the paths in one folder. Nothing else is required for the canary.
permit/
permit.json
check_permit.py
job.sh
fixtures/good_note.md
fixtures/thin_note.md
sandbox/notes/
sandbox/out/
evidence/
The JSON file is the contract. The Python file is the lock. The fixtures catch fake success. job.sh is the only entrypoint I run.
1. Write the permit card
Every field is evidence or a hard limit. No field is a slogan. Copy this and change the owner.
{
"version": 1,
"job": "summarize-notes",
"owner": "sam",
"lane": "local-canary",
"created_at": "2026-09-20T10:00:00Z",
"blast_radius": {
"root": "sandbox/notes",
"glob": "*.md",
"max_files": 20,
"max_bytes_out": 200000,
"deny_globs": ["*.env", "*.pem", "secrets/**"]
},
"evidence": {
"dry_run_log": "evidence/dry_run.jsonl",
"golden_log": "evidence/golden.jsonl",
"replay_cmd": "evidence/replay.sh",
"prev_out_hash": "evidence/prev.sha256",
"ack_file": "evidence/ack.txt",
"human_double_ack": "evidence/double_ack.txt"
},
"fail_closed": {
"missing_evidence": "refuse",
"lane_mismatch": "refuse",
"glob_escape": "refuse",
"golden_fail": "refuse",
"empty_model_output": "refuse",
"unstructured_output": "refuse"
}
}
Would you ship without a named owner? I would not. An orphan job has no rollback person.
real-write stays locked until human_double_ack exists. That extra file is a second timestamped yes. One ack is for canaries. Two acks are for real files. Does that feel slow? Slow is the point.
2. Add a golden fixture that can fail
Empty success is the failure mode this permit targets. Force a shape. Force bullets. Force a source hash.
fixtures/good_note.md:
Meeting 2026-09-19
Need a permit file before writes.
Owner is sam. Lane is local-canary.
fixtures/thin_note.md is a blank line on purpose. A thin source must not produce a fat summary. That mismatch is the pretend-work bug. A model can invent bullets from silence. The process exit code can still be zero.
Expected golden line:
{
"ok": true,
"title": "Permit before writes",
"bullets": ["Need a permit file", "Owner is sam"],
"source_sha256": "replace-with-real-hash"
}
If the model returns a paragraph, that line fails. If it returns ok: true with empty bullets, that also fails. Did the work happen? The fields have to prove it.
3. Build the checker
This script never calls a model. It only reads files. Run it before every write.
#!/usr/bin/env python3
"""Fail-closed production permit checker. No model calls."""
from __future__ import annotations
import json
import stat
import sys
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parent
PERMIT = ROOT / "permit.json"
ALLOWED_LANES = {"local-canary", "free-server-canary", "real-write"}
def die(code: int, msg: str) -> None:
print(f"permit refuse: {msg}", file=sys.stderr)
raise SystemExit(code)
def load_permit() -> dict:
if not PERMIT.exists():
die(2, "permit.json missing")
data = json.loads(PERMIT.read_text(encoding="utf-8"))
if data.get("version") != 1:
die(2, "unsupported permit version")
return data
def check_lane(p: dict) -> None:
lane = p.get("lane")
if lane not in ALLOWED_LANES:
die(2, f"lane {lane!r} is not allowed")
ev = p.get("evidence") or {}
if lane == "real-write":
ack2 = ROOT / ev.get("human_double_ack", "missing")
if not ack2.exists() or ack2.stat().st_size == 0:
die(2, "real-write needs human_double_ack evidence")
def check_blast(p: dict) -> None:
br = p["blast_radius"]
root = (ROOT / br["root"]).resolve()
try:
root.relative_to(ROOT.resolve())
except ValueError:
die(2, f"blast root escaped permit dir: {root}")
if not root.is_dir():
die(2, f"blast root missing: {root}")
if root == Path("/") or len(root.parts) < 3:
die(2, "blast root is too wide")
files = list(root.glob(br["glob"]))
if len(files) > int(br["max_files"]):
die(2, f"too many files: {len(files)}")
for g in br.get("deny_globs", []):
if list(root.glob(g)):
die(2, f"deny glob matched: {g}")
def check_evidence(p: dict) -> None:
ev = p["evidence"]
required = [
"dry_run_log",
"golden_log",
"replay_cmd",
"prev_out_hash",
"ack_file",
]
for key in required:
path = ROOT / ev[key]
if not path.exists() or path.stat().st_size == 0:
die(2, f"missing evidence: {key}")
ack = (ROOT / ev["ack_file"]).read_text(encoding="utf-8")
owner = p.get("owner") or ""
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
if owner not in ack or today not in ack:
die(2, "ack_file must contain owner and UTC date")
replay = ROOT / ev["replay_cmd"]
text = replay.read_text(encoding="utf-8").lower()
banned = ("model", "complete", "prompt", "temperature")
if any(word in text for word in banned):
die(2, "replay_cmd must not call a model")
if not (replay.stat().st_mode & stat.S_IXUSR):
die(2, "replay_cmd is not executable")
def check_golden(p: dict) -> None:
path = ROOT / p["evidence"]["golden_log"]
rows = []
for line in path.read_text(encoding="utf-8").splitlines():
if line.strip():
rows.append(json.loads(line))
if not rows:
die(2, "golden_log empty")
for row in rows:
if row.get("ok") is not True:
die(2, "golden row not ok")
bullets = row.get("bullets") or []
if not isinstance(bullets, list) or len(bullets) < 2:
die(2, "golden bullets too thin")
if not row.get("source_sha256") or not row.get("title"):
die(2, "golden missing title or source_sha256")
def main() -> None:
permit = load_permit()
check_lane(permit)
check_blast(permit)
check_evidence(permit)
check_golden(permit)
print("permit ok:", permit["job"], permit["lane"])
if __name__ == "__main__":
main()
Exit 2 means refuse. Exit 0 means the job may start. Any other exit is a bug in the checker. Do not ignore that. Should a warning count as a pass? No. Warnings are how quiet writes happen.
4. Wrap the job so humans cannot skip it
I do not run the model command by hand. I run job.sh. If I run the model by hand, the permit is theater.
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
python3 ./check_permit.py
# Label: example only. Swap in your local free-model CLI.
# Do not paste tokens. Do not point at real notes on the first run.
python3 ./summarize.py \
--root ./sandbox/notes \
--out ./sandbox/out \
--format json
python3 - <<'PY'
from pathlib import Path
import hashlib
out = Path("sandbox/out")
lines = []
for p in sorted(out.glob("*.json")):
digest = hashlib.sha256(p.read_bytes()).hexdigest()
lines.append(f"{digest} {p.name}")
Path("evidence/prev.sha256").write_text("\n".join(lines) + "\n")
print("wrote evidence/prev.sha256")
PY
Notice the replay script is checked first. Replay must rebuild output from saved JSON. Replay must not call a model. Can you rerun last Tuesday without a provider? That is the gate.
A labeled evidence/replay.sh can be this small:
#!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
import json
from pathlib import Path
path = Path("evidence/golden.jsonl")
for line in path.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
row = json.loads(line)
assert row["ok"] is True
assert len(row["bullets"]) >= 2
print("replay ok")
PY
For the first local canary, seed prev.sha256 with NONE plus the date. Real-write later must replace that seed. Real digests only, no slogans.
5. Run the failure fixture before a happy path
Never start with a green run. Start with a missing file. What does your current wrapper do when ack is gone?
mkdir -p sandbox/notes sandbox/out evidence
printf 'Meeting note\nNeed a permit.\n' > sandbox/notes/a.md
printf 'NONE 2026-09-20\n' > evidence/prev.sha256
printf '{"dry": true}\n' > evidence/dry_run.jsonl
cat > evidence/replay.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
import json
from pathlib import Path
path = Path("evidence/golden.jsonl")
for line in path.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
row = json.loads(line)
assert row["ok"] is True
assert len(row["bullets"]) >= 2
print("replay ok")
PY
EOF
chmod +x evidence/replay.sh check_permit.py job.sh
# Fixture A: missing ack must refuse.
rm -f evidence/ack.txt
python3 check_permit.py; echo "exit:$?"
# expect: permit refuse: missing evidence: ack_file
# Fixture B: dated ack, thin golden must still refuse.
printf 'sam 2026-09-20 local-canary\n' > evidence/ack.txt
printf '{"ok": true, "title": "x", "bullets": [], "source_sha256": "abc"}\n' > evidence/golden.jsonl
python3 check_permit.py; echo "exit:$?"
# expect: permit refuse: golden bullets too thin
# Fixture C: structured golden may pass the checker.
printf '{"ok": true, "title": "Permit before writes", "bullets": ["Need a permit file", "Owner is sam"], "source_sha256": "abc"}\n' > evidence/golden.jsonl
python3 check_permit.py; echo "exit:$?"
# expect: permit ok: summarize-notes local-canary
Did it refuse twice? Good. Only then run job.sh against sandbox/notes. A first green run teaches you nothing.
6. Promote lanes on purpose
Lanes are not decorations. They change the required evidence. I promote in this order, and I do not skip.
-
local-canarywrites inside./sandboxonly. -
free-server-canarycopies the same permit onto the free server. Thencheck_permit.pyruns there before any job process starts. -
real-writeneeds the double ack and a previous hash. That hash must match last good output.
Do not hop from local to real writes. The free server is a second witness, not a shortcut. If the permit fails on the server, the laptop result was luck. Would you accept luck as a release signal? I would not.
When I move to the free-server lane, I copy the folder first. I run the checker remotely next. I do not start the model over SSH yet. I wait for exit code zero. The command sequence stays boring on purpose.
rsync -a --delete ./permit/ user@free-server:~/permit/
ssh user@free-server 'python3 ~/permit/check_permit.py'
Hostnames here are placeholders. Use whatever host you already have. The point is the checker travels. The point is not a new dashboard.
Gate table you can copy
| Gate | Evidence | Fail-closed action |
|---|---|---|
| owner |
owner field |
refuse, do not infer |
| lane |
lane plus ack rules |
refuse unknown lanes |
| blast radius | root, glob, max files | refuse escape and overflow |
| deny glob |
*.env, *.pem
|
refuse on any match |
| dry run | evidence/dry_run.jsonl |
refuse if missing or empty |
| golden | evidence/golden.jsonl |
refuse thin bullets |
| replay | evidence/replay.sh |
refuse if it calls a model |
| previous hash | evidence/prev.sha256 |
refuse real-write without it |
| ack | evidence/ack.txt |
refuse if owner or date missing |
| double ack | evidence/double_ack.txt |
refuse real-write without it |
Print this table next to the repo. If a gate has no file, the answer is no. Can a Slack thumbs-up replace a file? Not for this permit.
What this does not prove
The permit does not prove the summary is true. It proves the job had a shape, a root, and a witness. Truth still needs a human skim on the first real batch.
It does not replace backups. Keep the previous output hash for a reason. If the new write is garbage, copy the old tree back. No hash means you are guessing.
It does not cap remote tokens. Put your own budget file in evidence if you care. I am not going to invent a provider quota here. A missing budget file is a product claim I cannot make.
It is not an enterprise control plane. It is a forty-five minute lock for a solo write job. If you need multi-tenant policy, this is the wrong artifact. If you need a design system, this is also the wrong artifact.
The banned-word scan in replay.sh is crude. A comment can trip it. Clock drift can fail the ack date. That is annoying and correct. Use UTC. Do not let local midnight pass a stale yes.
Who should not use this
Do not use this on secret trees. Deny globs are shallow. They will miss a new extension tomorrow.
Do not use this when you have no named owner. A shared ai-bot owner is how jobs become orphans. Who rolls back an orphan at 11pm?
Do not use this as a substitute for tests you skipped. The golden log is a canary, not a suite. Two bullets are not a spec.
Skip it if your tool only prints to stdout. No write, no permit. A viewer CLI does not need this lock. Would you add ceremony to a dry pager? Please do not.
A quiet next step
If you already use a free-model lane, start smaller. Run this permit against a fixture tree first. That is the whole ask.
Which gate would your current write job fail first?
MonkeyCode provides free models that can run this workflow.
Top comments (0)