Case study
Cairn
The guardrails under every project on this site: what AI coding agents may do without a review, hooks that refuse the rest before it runs, and reviewers that were tested before they were trusted.
- Role
- Sole author: the policies, the hooks and checks, the multi-agent pipelines, and the record they all write to.
- Stack
- Python hooks on Claude Code's tool-call and turn events, JavaScript workflow scripts for the pipelines, a POSIX shell push gate, GitHub Actions, and MCP servers for GitHub and the databases.
- Timeframe
- July 2026 to now. Still in progress.
- Links
- The repository is private. I can walk through it on request: dave@madcowhq.com.
The problem
The projects on this site were built with AI coding agents doing much of the typing. An agent works from an instructions file, and that file is context, not configuration: the tool’s own documentation says there is no guarantee of strict compliance. Every rule I had for the agents lived in one, and the record shows the rules losing.
One rule shows the shape. When an agent needed a decision from me, it was to offer clickable choices rather than ask in prose. It was broken seven times in two days, twice after I had reworded it to close the gap it slipped through. A turn that hands over a decision in prose looks exactly like a finished turn, so nothing noticed until I did.
The second failure looked like success. On 9 August the guards I had already written were registered nowhere. The settings file that should have switched them on had no hooks section at all, while every guard passed its own self-test. A guard that cannot launch does not refuse anything. It lets the call through, and the layer reads as installed.
The agents’ reviews needed the same suspicion. An early review pipeline handed five findings to an agent told to refute them, and it refuted none. That is either five good findings or a refuter that agrees with whatever it is handed, and from outside the two look the same.
What I built
First, a written ceiling on what automation may do without me. An agent may read, branch, commit, push its own branch, open a draft pull request and comment. It may not merge, mark a pull request ready, force-push, close or rewrite an issue, or delete anything. The ceiling is a draft because a wrong draft costs one review to discover, where a wrong merge or a closed issue costs a recovery.
Then the hooks: eight scripts that run on the agent’s tool calls and turns, before anything happens. Between them they refuse merges, force pushes and every spelling of branch deletion, work in a repository outside the session’s scope, a story filed without its epic and estimate, a pull request that would close an issue with unticked acceptance criteria, and a turn that ends on a question asked in prose. A git push gate refuses integration branches outright, and thirteen consistency checks keep the record honest. One of them fails the build when a number written in prose stops matching the value something else computes, which turned out to be the most common defect in the repository.
Seven multi-agent pipelines do the larger reads: a review of a working diff through six independent lenses, a sweep of every repository for work that shipped but was never recorded, backlog grooming, and others. In each, every finding goes to a separate agent instructed to refute it and to call it refuted when unsure. Only survivors reach me, usually with a correction attached. An agent that dies mid-run is reported as dead and never as a clean result. Four of the six pipelines that existed on 7 August had been counting a dead refuter’s finding as refuted, which is the most confident wrong answer a review can give.
Under it all is the record: 510 notes, each a decision with its reasoning or a mistake with its correction; a ledger row for every pipeline run, written by the session that dispatched it rather than by the pipeline; and a register of 98 faults that counts which ones came back after they were fixed.
A decision I’d defend
A rule an agent must not break is enforced outside the agent, and nothing counts as enforcement until it has refused something on purpose.
The first half came from the clickable-choices rule. A rule that comes back after its wording has been corrected has a delivery problem, not a wording problem, so the fix was a hook that reads the end of every turn and hands the turn back when it closes on a prose question. The same reasoning put merges, force pushes and deletions behind a hook rather than behind a sentence.
The second half came from 9 August. Every guard then had a passing self-test, and none of them was running. So now each guard is made to refuse on purpose before it is trusted: its test is mutated until it goes red, a check confirms the guards are registered on the machine where they run, and the proof of an install is a refused command rather than a green test. That discipline found a push gate that waved a protected branch through whenever git’s list of refs ended without a newline, and nine patterns in the question hook that could never have matched.
It also found the case I would least have predicted. A merge from the shell was refused while the same merge through a GitHub tool server was allowed, because the guard read the shell command and a tool server’s call carries none. A guard keyed on how an act is spelled misses the next spelling of it. Matching the tool by name closed that, and narrowing the token behind the server closed it a second time, so two independent ceilings now hold.
What it rejected, and why:
- Rewording the rule again. It had been reworded already, and two of the seven violations came after that.
- A guard on every hazard. Some failures have no clean signature in a command line. A guard that fires on half of them reads as coverage and stops anyone looking, so those stay written rules on purpose, with the reason recorded beside them.
- A passing self-test as proof of a guard. That was the state on 9 August, exactly.
- Letting a pipeline write its own run record. A pipeline’s account of itself is the self-assessment the rest of this exists to avoid.
The cost is real. A hook refuses some legitimate work, once including its own commit, and when a guard’s only false positives are cosmetic I pay them rather than soften it. A push gate on one machine stops a habit, not an adversary. And a hook catches a failure without curing it: sessions still reach for the prose question now and then, and the hook still hands those turns back.
Outcome
From 3 August to 1 October 2026 the pipelines ran 92 recorded dispatches with 1,533 agents and about 204 million tokens of agent work. Each has a ledger row saying what was found, what survived the refuter, and what was corrected.
The refuting stage was tested before I relied on it. Six claims with answers written down before the run, each dispatched twice, came back 12 of 12. The sweep’s confirming stage was tested the same way with the polarity reversed, and also matched 12 of 12. The test corpus was 45 lines and synthetic, which is easier than a real diff, and the record says so.
All eight hooks carry a self-test, as do ten of the thirteen checks; the other three were proven by mutation in the commits that added them.
What this page cannot give is a defect-escape rate. There is no before-and-after measure of delivery here, and none is implied. Still in progress.
The projects it guards are on the work index, each with what was decided and why.