The goal is small. Let a free model tidy one config file. The risk is not small. A model that writes files is a program with your permissions.
I have written enough of these checklists as prose. Nobody runs prose. So this time the checklist is a script. It exits non-zero. It writes evidence. It refuses to continue.
Here is the whole thing: seven gates, one evidence file, and a hard rule that a machine cannot approve itself.
Why exit codes beat checkboxes
A checkbox is a promise. An exit code is a fact. When the gate fails, set -e stops the pipeline before the write happens.
That is the difference between "we usually check" and "it cannot proceed". Ask yourself which one you have right now.
Step 1: Pick the smallest job you can actually gate
Do not start with a refactor. Start with one input file and one output file. My rule: the job must fit in a single diff a human can read in 60 seconds.
Write the plan down as text first. The plan is an input to the gates, not documentation for later.
job: tidy-config
reads: ./input/app.toml
writes: ./out/app.toml
budget: 20000 tokens, 3 requests
rollback: git stash + file snapshot
If you cannot fill in the writes line, stop. The scope gate has nothing to enforce.
Step 2: Put the gates in one file
Every gate is a command. Pass means exit 0. Anything else means stop.
#!/usr/bin/env bash
# readiness.sh — fail-closed checklist for one AI task
set -euo pipefail
JOB="${1:?usage: readiness.sh <job-dir>}"
EVID="$JOB/evidence.jsonl"
: > "$EVID"
gate() { # gate <name> <command...>
local name="$1"; shift
local stamp; stamp="$(date -u +%FT%TZ)"
if "$@" >/dev/null 2>&1; then
printf '{"gate":"%s","status":"pass","at":"%s"}\n' "$name" "$stamp" >> "$EVID"
else
printf '{"gate":"%s","status":"fail","at":"%s"}\n' "$name" "$stamp" >> "$EVID"
echo "FAIL-CLOSED: $name" >&2
exit 1
fi
}
gate boundary ./checks/secrets.sh "$JOB/input"
gate scope ./checks/paths.sh "$JOB/plan.txt"
gate budget ./checks/budget.sh "$JOB"
gate failure ./checks/inject.sh "$JOB"
gate rollback ./checks/snapshot.sh "$JOB"
gate diff ./checks/diffsize.sh "$JOB"
gate signoff ./checks/signature.sh "$JOB"
echo "ready: $EVID"
Seven lines of gate calls. That is the whole checklist, and it is diffable in review.
Step 3: Know what each gate actually proves
A gate that proves nothing is decoration. Here is the honest column.
| Gate | Automatable | Evidence written | Fail action |
|---|---|---|---|
| boundary | yes — regex + deny-list | matched file list | stop, redact, re-run |
| scope | yes — allowlist diff | unexpected write paths | stop, revert |
| budget | yes — hard counter | request/token count | stop mid-run |
| failure | yes — bad fixture | non-zero exit + stderr | stop, file a bug |
| rollback | yes — snapshot hash | restore test log | stop, fix restore |
| diff | yes — line count | diffstat | stop, split the job |
| signoff | no — human only | name + timestamp | stop, ask |
Six of seven gates are mechanical. The seventh is a person. Keep it that way.
Step 4: Give the budget gate teeth
A budget that logs and continues is not a budget. Wrap the call and count before you send.
# call.py — one request, one ceiling, no retries
import os, json, urllib.request
BASE = os.environ["BASE_URL"] # your provider's endpoint
MODEL = os.environ["MODEL"]
CAP = int(os.environ.get("TOKEN_CAP", "20000"))
spent = 0
prompt = open("input/app.toml").read()
estimate = len(prompt) // 4 # rough, deliberately pessimistic
if estimate > CAP:
raise SystemExit("FAIL-CLOSED: budget")
body = json.dumps({"model": MODEL, "messages": [{"role": "user", "content": prompt}]}).encode()
req = urllib.request.Request(BASE, data=body, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=60) as r:
out = json.load(r)
spent += estimate
open("evidence/budget.json", "w").write(json.dumps({"estimate": spent, "cap": CAP}))
print(out.get("choices", [{}])[0].get("message", {}).get("content", ""))
No retry loop. A retry loop is how a 20k budget becomes a 60k bill. If the call fails, the gate fails, and a human decides.
Step 5: Where a free host fits
MonkeyCode is an open-source project, and the hosted side advertises free model access plus a free server option. The team currently lists a free token allowance in the 10-million range; free-tier numbers move, so read the project page for today's figure instead of trusting a blog post — including this one.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
What that buys you here is the ability to run the failure gate against a real endpoint without spending money. That is the point of a free tier in a checklist workflow: you need to see the bad path, not just the happy one.
The free server also removes a hardware decision from the critical path. You can run the same readiness.sh on a box that is not your laptop, which is exactly what the rollback gate wants.
Set BASE_URL and MODEL from whatever the provider documents. Do not hardcode a model name into a gate script. Names age badly and your checklist should outlive them.
Step 6: Ship a failure fixture before you ship the feature
Write one input that must fail. Mine is a config with a duplicate key and a trailing secret-shaped string.
./readiness.sh jobs/tidy-config
# FAIL-CLOSED: boundary
# exit 1
The output above is the shape to expect, not a benchmark. If your bad fixture passes, your boundary gate is a comment with extra steps.
When to abandon the whole thing
Set kill criteria before you start. Mine, in order:
- Two gates cannot be automated in one hour → park the job.
- The rollback restore fails twice → the write is not safe yet.
- The diff exceeds 60 lines → the job was too big, split it.
- The free tier queues past your patience → pay or shrink the job, do not remove the gate.
Criteria 4 matters. The temptation is to disable a gate when the host is slow. Delete the gate, delete the evidence, and you are back to vibes.
Limitations, stated plainly
These gates prove process, not model quality. A model can pass all seven and still produce wrong config. You still need a test.
They also assume you control the write path. If your tool writes directly to a database, the snapshot gate is a fiction — do not use this pattern there.
Skip this if you handle regulated data, need an SLA, or cannot get a human signature within the same day. The signoff gate will block you forever, and that is correct behavior for you.
For solo builders and small teams shipping one small AI feature, the script is roughly 80 lines, one afternoon, zero dependencies.
One question that would change the next build
What is the gate you cannot automate — the one where you keep adding a manual step and it keeps getting skipped? That specific step is where I would put the signature check next.
Top comments (0)