Deterministic guardrails for AI-written code
When an autonomous agent writes a large share of your code, the bottleneck moves from writing to reviewing — and careful review does not scale. You can read the first ten diffs closely. By the fiftieth, your eye is sliding over confident-looking code that is wrong in a way you would have caught on day one. So I stopped relying on attention for anything a machine can decide, and turned every standard I care about into a deterministic guardrail .
A guardrail, here, has a precise meaning: a program that walks the repo, finds violations, and exits non-zero. Not a doc, not a lint suggestion, not a line in a style guide nobody reads. A check that fails the build. The standard is encoded once and enforced forever — identically, regardless of whether a human or a model wrote the diff, and regardless of how tired either of them was.
This is the system I run on ralphy, my autonomous-agent harness. There are about thirty of these guards now. This is what they are, and — the part I’d missed until recently — where they have to run.
Enforcement layers
The familiar enforcement points — pre-commit, pre-push, CI — are the right backbone for human teams. But they share a blind spot when the author is an agent: they all fire after the code exists. By the time a pre-commit hook rejects a bad edit, the model has already spent tokens writing it, and you’re paying for a retry loop instead of preventing the mistake.
So the earliest layer isn’t pre-commit, and it isn’t even a check. It’s the agent’s own instructions. From earliest to last:
AGENTS.md— every rule is also written into the agent’s instructions, injected into each prompt, so the model aims to comply from the first token.- AI hooks — intercept the model as it works, before the code is even written.
- pre-commit — fast structural checks on staged files, so the inner loop stays tight.
- pre-push — the heavier suite: duplication, test integrity, typecheck, build.
- CI — the same commands again, with a parity check that proves local and remote agree.
The interesting work of the last two years was moving guardrails leftward — first into hooks, and ultimately into the prompt itself.
Layer zero: write the rules into the prompt
Every check in this post has a twin: a plain-English rule in AGENTS.md. This matters more than it looks. A guardrail that only fails the build is correction — the model writes the violation, hits the wall, and burns a round-trip rewriting it. The same rule stated up front in the agent’s instructions is prevention — the model reads “names are spelled out, no abbreviations” and simply doesn’t write cfg in the first place.
That’s the difference between a tight loop and a thrashing one. Left to a wall alone, an agent can spend three or four iterations bouncing off the same guard. Stated in AGENTS.md, most of those violations never happen, and the deterministic check becomes a backstop for the rare miss rather than the primary teacher. Cheaper in tokens, faster in wall-clock, and far less infuriating to watch.
The risk, of course, is drift: the prompt says one thing while the build enforces another, and now the agent is being lied to. That’s exactly what the prompt-rule-sync guard (below) exists to prevent — the instructions and the checks are kept provably in step, so writing the rule down twice can never mean writing it down two different ways.
The AI hooks
The prompt asks for compliance; hooks enforce it the moment the model acts. These are Claude Code hooks — small scripts the agent runtime calls on specific events. They don’t review the code after the fact; they shape what the agent is allowed to do in the first place.
- Block edits to the validators themselves. A
PreToolUsehook onEdit/Writehard-blocks the agent from touching the frozen check scripts. This is the load-bearing one: without it, an agent stuck on a failing guard will “fix” the build by editing the guard. The hook makes the guardrails immutable from inside the loop — the model can satisfy them or stop, but it can’t disable them. - Block a PR with duplicates. A
PreToolUsehook onBashwatches forgh pr createand runs the duplicate-declaration check first. A PR that would introduce a secondPaymentStatusenum never opens. - Force test-first on bug fixes. A
UserPromptSubmithook recognizes a bug-fix request and reminds the agent to follow the test-first workflow — a failing test that reproduces the bug before any production edit. - Pattern blocks. Mechanical blocks on specific anti-patterns: no
console.*in application code (it goes through the observability seam), no duplicate enums, no comments that just restate the code, observability required in catch blocks.
The point of gate one is economic as much as architectural: the cheapest violation is the one the model never writes. Every guardrail you can express as a pre-tool-use block is one the agent learns to design around instead of repeatedly bouncing off.
The catalog
The thirty-odd guards, grouped by what they protect. Each is a single script with one job; most are a few dozen lines.
Types & data shapes
- No unchecked casts —
asacross a boundary you don’t own is rejected. Parse, don’t cast. - Strict
any:noImplicitAnyplus a lint rule banning explicitany. - Event-union integrity — the discriminated unions that cross the wire can’t drift out of sync between producer and consumer.
Architecture & boundaries
- No direct HTTP in the wrong layer — application code can’t
import axios; it goes through a typed client. - Tag boundaries — the
scope:*tags on each package are load-bearing: a package may depend only on packages of equal or lower rank in the dependency DAG. - Tracker seam — the orchestration core depends only on tracker-neutral shapes, never on a concrete issue-tracker SDK.
- One config pipeline — configuration is merged in exactly one place, never re-derived ad hoc.
- Orphan-package detection — a whole package that nothing imports fails the build (the dependency cruiser only catches orphaned files).
Structure & layout
- Per-file LOC budget, and per-folder file count — files and folders that grow past the cap fail.
- One component (or hook) per file; hooks only in
useSomething.ts. - Tests co-located with their source; no re-export-only files; kebab-case filenames.
- No prop-drilling — an AST pass fails the build if a prop is threaded through three or more layers without being read.
Naming & comments
- No abbreviations — identifiers are spelled out.
- Static error messages — error constructors take constants, not interpolated strings, so failures stay searchable.
- No obvious comments — a comment has to earn its place.
Runtime & platform
- Bun-native APIs — synchronous
node:fsis banned in source.
Duplication
- One duplication check orchestrating four detectors: same-name declarations across files (a custom AST walk), the TypeScript “duplicate identifier” signal, a static-analysis pass, and copy-paste detection.
Tests
- Test integrity —
.only(which silently hides every other test in a file) and stray.skipfail the build.
Dependency hygiene
- Circular-dependency detection, unused-export (dead-code) detection, and an outdated-dependency check with a bounded drift budget.
Meta — guards on the process itself
- CI parity — any
check-*guard a developer runs locally must also run in CI. This closes the “stricter on my machine than in the pipeline” gap, which is how guardrails quietly rot. - Branch-protection drift — the live protection rules on
mainare diffed against a checked-in config. - Stale-change detection — a completed unit of work that didn’t get archived is flagged.
- Prompt-rule sync — the engineering rules injected into the agent’s prompt are kept in sync with the checks that actually enforce them, so the model is never told one thing while the build enforces another.
Ratchet, don’t rewrite
A rule you can only adopt with a big-bang migration never gets adopted. So several of these guards are ratchets: they grandfather the violations that already exist and enforce the rule only on new and changed code. The no-abbreviation rule, the Bun-native rule, the single-config-pipeline rule — each carries a frozen baseline of known exceptions and fails only when that baseline grows.
That changes the economics of raising a standard. You don’t need a clean weekend and a 400-file PR. You merge the guard with today’s debt frozen in place, and from that commit forward the number can only go down. The codebase improves monotonically without ever blocking a feature.
The guards guard themselves
The subtle failure mode of any quality gate is that it erodes from the inside — someone weakens a rule to land a deadline, local and CI drift apart, the agent edits a validator to get unstuck. Three guards exist only to prevent that:
- The protected-edits hook makes the validators immutable from inside the agent loop.
- The CI-parity guard makes it impossible for a local check to silently not run in CI.
- The prompt-rule-sync guard keeps what the agent is told aligned with what the build enforces.
Without this layer the whole system is theatre — a wall with a door the model props open the first time it’s inconvenient.
The honest version
None of this makes the code correct. A type system stops any; it does not stop a logic error in a perfectly-typed function. No folder-size cap ever caught a wrong business rule, and no duplication detector knows whether the thing you wrote should exist at all. Correctness is still mine, and still a reviewer’s — that part doesn’t delegate.
What the guardrails do is hold the floor. They let me hand a large amount of code to an agent and trust that the result clears the same bar as the code I wrote by hand: same types, same layering, same structure, same naming, no smuggled console logs, no second copy of an enum that already exists. The model writes fast; the guardrails make sure fast doesn’t mean the codebase quietly rots — and they do it deterministically, so the standard never depends on how carefully anyone reviewed that day.
The principle underneath all thirty: if a standard matters, don’t write it in a doc. Write it as a check that fails the build — and run that check as early as the author will let you.
Filed under engineering. Building your own guardrails — or think I’ve over-engineered mine? Compare notes.