Enforcing Invariants in AI-Generated Code with ADRs and Contracts
Bit Byte Bit
SubscribeSign in
Enforcing Invariants in AI-Generated Code with ADRs and Contracts<br>AI-generated code looks correct but quietly breaks constraints you assumed were safe. Enforce invariants with ADRs and contracts the agent can't bypass.
Zarar Siddiqi<br>Jun 30, 2026
Share
I had earlier written about using Hooks to enforce certain rules in AI-generated code. The idea was to use deterministic checks rather than prose-based guidance which can’t be enforced at time of generation. A hook here is just a script that runs at a point in the agent’s lifecycle and can block it from continuing. The broader idea is to enforce invariants and in this post I will show how we can use:<br>Classic Architectural Decision Records (ADRs) to record and enforce invariants
Use the RFC 2119 keywords like SHALL and MUST to record and enforce invariants
But first, what is an invariant? Borrowed from Domain-Driven Design, an invariant is a rule that must always hold true for your system to be in a valid state. It’s a promise the code makes to itself that this condition is never allowed to break. An LLM will produce code that looks correct yet quietly violates a rule you assumed was safe, and it has no inherent memory of the constraints your system depends on. The job, then, is to encode those invariants where the AI can’t ignore them, so that generated code is forced to honour them.<br>Architecture Decision Records as Invariants
Decisions you have made about your architecture can be thought of as invariants that need to be followed. As you incrementally make decisions, we need to ensure they are recorded so agents can treat them as invariants (i.e., rules to be followed). ADRs provide a structured method to do this. To take advantage of them, we need to:<br>Figure out when we need to record one
Actually record it
Point agents to treat the ADRs as invariants
Knowing when to record one
The hardest part is knowing when to record one. Architectural decisions rarely announce themselves as they slip by inside an ordinary coding session when you pick one storage approach over another, add a major dependency, introduce a new abstraction, or replace an established pattern. To catch these, I use an ADR auto-suggest skill that watches the shape of a conversation and flags an architectural inflection point as it happens. It looks for the tell-tale signals. An “X vs Y” comparison, “replace X with Y” or “deprecate X” language, the introduction of a new system service, or any pattern-setting choice that future work will inherit. It deliberately ignores bug fixes, styling, and behaviour-preserving refactors so it doesn’t fire on noise. The skill never writes the record itself and its only job is to recognize the moment and steer me toward the /adr command. It runs passively most of the time, but I can also invoke it manually whenever I want a second opinion on whether a decision I’m about to make deserves to be recorded.<br>Recording it
Once a decision is worth capturing, the /adr command does the mechanical work. It finds the next sequential ID, creates a new NNNN-short-kebab-title.md file, and fills in the frontmatter and template: a Context section for the “why now”, a Decision stated in a sentence or two, the Options considered with their trade-offs, and the Consequences that follow. The ADR is staged alongside the related feature commit, so there’s history next to the related code rather than in a separate commit (example ADR here). Just as importantly, the command keeps an index page current with a table of live decisions and one for superseded decisions, so there is always a single, ordered map of every invariant the architecture has committed to.<br>Treating ADRs as invariants
Recording a decision is pointless if agents never read it and the index page is the entry point to all such decisions. I point the agent at it so that before touching anything architectural it consults the relevant ADRs and treats their Decision sections as hard constraints. I add a deterministic check in the same spirit as the Hooks from before that verifies the ADRs were actually consulted before code is allowed through. The check confirms that any change touching an area governed by an ADR references that ADR, and fails the run otherwise. This closes the loop: the decision is recorded, surfaced, and then enforced, so an invariant can’t be silently violated simply because the agent didn’t bother to look.<br>Below is a Stop hook which runs when the agent thinks it’s finished. Like every Claude Code hook it receives a blob of JSON on stdin, which is where the path to the session transcript comes from. Each ADR declares the paths it governs as a scope list of globs in its frontmatter, and the hook compares the files changed in the working tree against those globs. If a changed file falls under an ADR’s scope, that ADR has to have been opened during the session, which I detect by scanning the...