Guardrails first: how I make AI do the right job, fast
Since August 2025 I haven't written much code by hand. Claude Code writes almost all of it, and my job has moved from typing to deciding what gets built and building the guardrails that keep the agents honest.
I didn't get there in one step. I started where most people start, by writing rules for the agent in its config files. When those stopped working I moved to written specs, and when specs weren't enough I moved the rules into code: custom lint rules, dependency checks and other static analysis. This is that story, using the rewrite of RemoteLock's web app as the example.
The example: rewriting RemoteLock's web app
At RemoteLock we are rebuilding the customer web app as a new React single-page app, connect-web-v2. It is a good stress test for AI-driven development for three reasons:
- It ships next to the old app. Both apps answer the same URLs, and a route manifest decides, path by path, which one serves a customer. Nothing can break the parts that are not moved yet.
- It is big and long-lived. Hundreds of pull requests since mid-2026, many of them written by agents, and a codebase other engineers will maintain for years.
- The old code is a trap. The legacy app works, but copying its patterns would copy its problems. The rule is that legacy is reference only: behaviour is re-specified from scratch, never ported.
That combination punishes an agent that guesses. So the repo is built to stop guessing.
Stage 1: rules in Claude config files
I started with CLAUDE.md: a file of conventions the agent reads at the start of every session. Use the component library. Keep modules apart. Test this way. Name things like that. It felt like onboarding a new engineer, and at first it worked.
Then I noticed the output wasn't consistent. The same rule held in one session and was ignored in the next. So I tried the obvious fixes, in both directions:
- More detail. Longer explanations, examples, the reasoning behind each rule. The file grew and the agent followed it no more reliably. Rules near the top competed with rules near the bottom.
- Less detail. Short, sharp lines. Easier to scan, but loose enough to be read several ways, and the agent picked whichever reading suited the task in front of it.
- Finer granularity. Rules split per folder and per kind of task. That helped locally, but which rules applied to a given change became its own guessing game.
Some changes made things worse. An example written to illustrate one rule got copied as a pattern where it didn't belong. A rule meant for one area leaked into others. And every line spent context that the actual task needed.
The lesson took a while to land: a config file is a request, not a guarantee. The model reads it the way a person reads a long wiki page, mostly right and never exactly the same way twice. Config files are still the right place for orientation, meaning where things live, why the code is shaped this way, and which docs to read. They are the wrong place to rely on for anything that must always be true.
Stage 2: specs with OpenSpec
The next problem was scope, not style. Asked to build a feature, the agent filled every gap with its own assumptions, and on a rewrite next to a legacy app those assumptions were often the legacy app's habits.
OpenSpec fixed that part. Every change starts as a written proposal: the behaviour as requirements and scenarios, and a task list. One spec change per pull request. The agent loads that change and the current spec for its area before it writes code, so it builds against a contract instead of a guess.
What it helped with:
- Less guessing. The agent no longer decides what a feature does. The spec does.
- Faster review where it counts. I read a page of behaviour before anything is built. A wrong spec is cheap to fix, and a wrong diff isn't.
- A fresh start from legacy. Behaviour is re-specified from scratch, so the old app stays a reference and never becomes a template.
- Small pull requests. One change per PR keeps each one reviewable in minutes.
What it didn't fix: a spec says what to build, not how the code should look. Two agents could implement the same spec correctly and still break the architecture in two different ways. The "how" was still living in a config file, so it was still a request.
Stage 3: rules as code
The change that made the output consistent was moving rules out of prose and into static analysis. If a rule matters, a machine checks it, and the check answers yes or no. The agent doesn't have to remember the rule, because it can't get past it.
In connect-web-v2 that means:
- Custom lint rules for our conventions. No raw HTML elements,
classNameor inline styles outside the UI library. Components are typed arrow constants. Packages are imported by name. Tests find elements only by test id. Each one started as a line in a config file that agents kept missing, and became a lint error. - No escape hatch. Disabling a lint rule is itself forbidden, so an agent under pressure can't silence a check to get green.
- Dependency rules for the architecture. dependency-cruiser enforces module boundaries: a module is imported only through its public entry file. The list of top-level modules is closed, and adding one needs a person's approval in the same PR.
- Presence checks. Every visual component must ship with a story and a test, or be explicitly tagged as non-visual. Every page must name itself in the browser tab.
- Dead code and drift. knip fails on unused files and exports. The API client is generated from the backend's OpenAPI schema, and CI fails if the two drift apart. Specs are validated too.
- Budgets and gates. An 80% coverage floor on logic, accessibility checks, bundle-size budgets, a changelog entry on every user-facing change, a phone screenshot on every UI change.
- Grandfather lists that only shrink. Old code that doesn't meet a new rule is listed by name, and the list can only get shorter. A new rule never waits for a big cleanup.
The dependency rules matter for another reason: they make the codebase a set of vertical slices. Each module owns everything for its feature in one folder: its API calls, schemas, hooks, components, permissions and tests, with sub-areas as nested modules of the same shape. A task almost always lives inside one slice. So the agent only needs that folder and the public entry files of the few modules it uses, a small context where everything is related, instead of a horizontal layer that cuts across the whole app. Smaller, related context is a big part of why the output got consistent: there is less to misread and less to leak into.
The effect on the agents was immediate. A red check is specific: it names the file, the line and the rule. The agent reads it and fixes it, usually on the first retry, without me in the loop. The config file changed role too. Its rules are still there, but each one now has a check behind it, so the file explains the rule and the check makes it true.
Stage 4: a review that can be argued with
Static analysis catches what can be stated as a rule. What's left, like whether the code actually does what the spec says, goes to an automated review that runs on every pull request against that PR's own spec. It is built so the model only notices and code decides:
- Several review passes read the diff and write candidate findings to files. They post nothing.
- A reconcile step matches them against a per-PR ledger of findings, and it is the only thing that comments.
- The verdict is a pure function of that ledger. Open critical or major findings block the merge, and nothing else does.
- Later rounds check whether the open findings were fixed instead of re-reviewing from scratch, so a review converges instead of finding new nitpicks forever.
When a finding is wrong, anyone can reply /review dispute with the reason. A fresh session, with none of the first reviewer's reasoning, re-derives the evidence and either withdraws or upholds the finding. A finding is a claim about the code, so it falls to evidence, not authority. Visual changes get the same treatment: a visual-regression diff on demand, approved per commit.
Put together, a change only reaches a person after every machine has said yes:
Highlighted: the two places a person reads
A red check or an open review finding sends the change back to the agent. I read only the spec at the start and the evidence at the end.
Why it's fast
Guardrails sound like friction. In practice they are what makes speed safe, because they move judgement out of my head and into places an agent can read and a machine can check.
- Small units of work. One spec change per PR keeps each change small enough to review in minutes and to revert in one click.
- Cheap rejection. A red check costs the agent a retry, not me a conversation. Most problems are caught and fixed before I ever look.
- Parallel work without collisions. Module boundaries and a closed module list mean several agents can work in different areas at once without stepping on each other.
- Review that converges. The ledger turns review into a checklist that empties, not an argument that grows. Disputes settle on evidence in minutes.
- Rules that compound. Every time an agent gets something wrong in a new way, the fix is a lint rule or a check, not another line in a config file. The next agent can't repeat the mistake.
The downside
This works so well that it creates its own problem. When every convention is a lint rule and every boundary is a check, writing code becomes almost deterministic. That's exactly what you want from an agent, and it's not great for the people working in the codebase.
- Less room to improve. A better pattern now has to beat a rule, a check and a grandfather list before anyone can try it. Experiments that would have been a small PR become a debate about changing the rules.
- Gaps get hidden, not found. The checks prove the code follows the architecture. They say nothing about whether the architecture is right. When everything is green, nobody is prompted to notice that a boundary is in the wrong place or that two modules really want to be one.
- Less learning by doing. Engineers who mostly read specs and green checks spend less time inside the code, which is where people usually spot that something doesn't fit.
I don't have a full answer yet. What helps so far: treating the rules themselves as code that gets reviewed and changed, keeping architecture decisions with people, and setting aside time to question the structure instead of only building inside it.
What still needs me
The agents write the code. Four things stay with me:
- Deciding what to build. The spec is the product decision, and it is the part I read most carefully.
- The shape of the system. Architecture decisions, new top-level modules and the rules themselves are reviewed by people.
- Taste. Whether a screen feels right, whether copy reads well, whether a flow makes sense to a property manager at 7am. I judge that from the screenshots and recordings each PR carries.
- Building the guardrails. When something slips through, I don't fix the code. I fix the rule, check or skill that let it through.
That last point is the whole method. The fastest way to make AI do the right job is not a better prompt. It is a repository where the wrong job can't pass.