If you work with AI coding agents every day, you have probably seen this: you ask for a small change, and the agent suggests swapping your authentication library, changing your data access pattern, or "simplifying" something that was built that way for a good reason.
The agent isn't doing this on purpose. It just doesn't know why your decisions were made.
I've been using Architecture Decision Records (ADRs) for years as a team practice. Since AI agents became part of my daily work, they stopped being "nice documentation" and became one of the most effective guardrails I have. This post explains why, shows a real example from one of my projects, and describes how I turn ADRs into guardrails that an agent actually follows.
Why agents keep reopening settled decisions
An agent like Claude Code, Cursor or Copilot knows a lot about software in general and very little about your project in particular. Three things make this worse than it sounds:
- No memory between sessions. Every session starts from zero. The agent only knows what is in its context: your prompt, the files it reads, and the instruction files you give it.
- Code shows the "what", never the "why". The agent can see that you use PostgreSQL. It cannot see that you evaluated MongoDB, rejected it because of reporting needs, and accepted the extra migration work on purpose.
- Without constraints, it falls back to generic best practices. If nothing says otherwise, the agent will optimize for what is usually a good idea, not for what is right in your context. Those are often not the same thing.
That "why" used to live in the heads of the people who had been on the team the longest. A new developer would ask, and someone would explain. An agent doesn't ask. It acts on what it can read. If the reasoning isn't written down, from the agent's point of view it doesn't exist.
What an ADR is
An ADR is a short document that records one architectural decision. It is not a design doc and it is not a wiki page that tries to describe the whole system. It captures a single decision at a single point in time.
A good ADR answers four questions:
- Context: what problem did we have, and what constraints were we under?
- Decision: what did we choose?
- Alternatives: what did we reject, and why?
- Consequences: what did we gain, and what did we give up?
This is the template I use. It is intentionally small:
# NNNN. Title
- **Status:** Proposed | Accepted | Superseded by NNNN
- **Date:** YYYY-MM-DD
## Context
What forces are at play (product goals, technical constraints, costs)?
## Decision
What we decided, stated in full sentences ("We will…").
## Consequences
What becomes easier or harder, the risks we accept, and the follow-ups this creates.
ADRs live in the repository, usually in docs/adr/, numbered in order, with an index in docs/adr/README.md. Two rules make them work over time:
- An accepted ADR is never rewritten. If the decision changes, you write a new ADR that supersedes the old one.
- Each ADR has a status. Readers (human or not) can tell which decisions are still in force.
A real example: when a decision has to change
I'm building a personal running app (React Native with Expo, a Node API, Strava integration). It has 32 ADRs so far. Here is a pair that shows why this matters with agents.
ADR 0004 says: the app does not record GPS. Strava is the single source of recorded activities. The reasoning is in the context section: the watch and Strava already record runs well, background GPS isn't available in Expo Go, and duplicating recordings would only add battery drain and complexity.
Without that ADR, an agent asked to "show the distance of each interval" would very likely add GPS recording. It is the obvious solution, and it would quietly break a decision I made on purpose.
Then a real need appeared. During track workouts with timed reps (for example, 10 × 30 seconds), I don't know how far I ran in each rep, and my coach asks for it. That is a different problem: I don't need a recorded activity, only the distance inside each fast window.
So I didn't "just add GPS". I wrote ADR 0020, which partly supersedes ADR 0004, for timed reps only. It records:
- the constraint: in Expo Go, location updates only arrive while the app is in the foreground;
- the decision: GPS is watched during sessions with timed reps, noisy fixes are filtered out, and only the metres per rep are kept, with no stored or uploaded positions;
- the consequences: a few metres of error per rep, extra battery use, and the screen must stay on. Measuring with the screen locked would need a development build and a new ADR.
The important part is the last line of that ADR: Strava stays the source of recorded activities (the rest of ADR 0004 holds).
Now, when an agent works on this code, it can read exactly where the line is. GPS is allowed for timed reps, under specific rules. It is still not allowed for recording runs. And if I ever ask for something that crosses that line, the agent has what it needs to stop and tell me: "this conflicts with ADR 0004 and ADR 0020".
That is the shift. The agent stops being something that might break your decisions and becomes something that can defend them.
From documentation to guardrails
Writing ADRs is the first step, but an ADR sitting in a folder is only useful if the agent reads it. Over time I ended up with four layers. Each one covers a weakness of the previous one.
Layer 1: ADRs hold the "why"
The ADRs are the source of truth for decisions and their reasoning. Everything else points to them.
Layer 2: the instruction file turns decisions into rules
Agents read an instruction file at the start of every session (CLAUDE.md, AGENTS.md, .cursor/rules, copilot-instructions.md, depending on the tool). This is where ADRs become short, explicit rules, each one citing the ADR that explains it.
Here is part of the CLAUDE.md from the running app:
Read `README.md` and `docs/adr/` before changing architecture.
## Rules
1. Every project decision becomes an ADR in `docs/adr/` (template there;
never rewrite an accepted ADR, supersede it). Update the index in
`docs/adr/README.md`.
2. `packages/domain` is framework-free: no React, Expo, Node or HTTP imports.
3. Mobile runs in Expo Go. Anything that needs a dev build (Face ID,
HealthKit, background GPS) requires an ADR first.
5. Strava is the activity source; never record activities with GPS (ADR 0004).
GPS only measures timed reps (ADR 0020).
7. Tokens only in secure storage; never in AsyncStorage/SQLite/logs (ADR 0005).
A few things make this work:
- The rule is short; the reasoning is in the ADR. The instruction file stays small enough that the agent reads all of it every time, and it can open the ADR when it needs the details.
- Each rule cites its ADR. When the agent pushes back, it can say which decision is at stake, and I can check whether that decision still makes sense.
- Some rules are gates, not prohibitions. "Requires an ADR first" doesn't forbid background GPS forever. It forces the conversation to happen before the code, which is exactly when I want to have it.
Layer 3: the process makes the agent part of the ADR workflow
Agents are good at writing ADRs. They are fast at summarizing context, listing alternatives and spelling out consequences. So I let them draft, and I decide.
My workflow looks like this:
- Before any structural change, the agent reads the relevant ADRs.
- If my request conflicts with an accepted ADR, the agent says so and names it, instead of silently working around it.
- If the decision really needs to change, the agent drafts a new ADR (status
Proposed) that supersedes the old one, and updates the index. - I review it, change what's wrong, and only then mark it
Acceptedand implement.
This keeps a human in charge of the decisions while the agent does most of the writing. It also means the history is never lost: six months later, both I and the agent can follow the chain from ADR 0004 to ADR 0020 and understand why the code looks the way it does.
Layer 4: automated checks enforce what can't be left to reading
Instruction files are strong guidance, not guarantees. Agents follow them most of the time, but not always, especially in long sessions or when the context gets crowded. So for decisions where a violation would be expensive, I don't rely only on the agent reading a rule. I make the build fail.
Some examples of decisions that can be enforced automatically:
-
Layer boundaries: a lint rule (for example,
no-restricted-importsin ESLint, or a dependency-cruiser config) that blocks framework imports inside the domain package. - Security decisions: in the running app, the API's security decisions are in an ADR, and a smoke test against the production image runs the matching attack checks: user enumeration, 2FA bypass, mass assignment, a tampered JWT, deleting another user's data (IDOR), refresh-token replay. If an agent "simplifies" the authorization logic, the check fails.
- Domain rules: unit tests that encode the rules an ADR describes, such as how GPS fixes are filtered.
- Agent hooks: many tools now support hooks that run before or after the agent edits files or runs commands. They are a good place for fast checks, like blocking edits to accepted ADRs or running the linter after every change.
-
A single "done" command: one command (
npm run checkin my case) that runs lint, type checks and tests, which the instruction file requires before a task is considered done.
A simple way to think about the layers:
| Layer | What it does | Its weakness |
|---|---|---|
| ADR | Explains why a decision was made | Only works if someone reads it |
| Instruction file | Turns decisions into rules the agent reads every session | Guidance, not enforcement |
| Process | Forces decisions to be discussed before code is written | Depends on discipline |
| Automated checks | Fails the build when a decision is violated | Only covers what can be checked |
None of these layers is enough on its own. Together, they cover each other's gaps. The ADR is what ties them together: every rule, gate and check can point back to a decision with a written reason.
What not to do
A few mistakes I've made or seen:
- Writing ADRs for everything. Naming a variable is not an architectural decision. If reverting the decision would be cheap, it probably doesn't need an ADR.
- Writing long ADRs. If an ADR needs ten pages, it is a design doc. Agents and humans both skip long documents. One or two screens is usually enough.
- Editing accepted ADRs. It feels faster, but you lose the history, and the agent loses the ability to understand how and why things changed.
-
Leaving superseded ADRs without a status. Two ADRs that contradict each other, both marked
Accepted, will confuse an agent more than having no ADRs at all. - Not linking ADRs from the instruction file. If the agent doesn't know the folder exists, it might never open it.
Getting started in an afternoon
If your project has no ADRs yet, you don't need to document its entire history. Start small:
- Create
docs/adr/with a template and a README index. - Write ADR 0001: "We record architecture decisions using ADRs".
- Write ADRs for the three to five decisions an agent (or a new developer) is most likely to undo: your data store, your authentication approach, your layering rules, and anything that looks "wrong" but is intentional.
- Add a short rules section to your agent's instruction file, with each rule pointing to its ADR, plus the instruction to read
docs/adr/before structural changes. - Pick the one decision that would hurt most if broken and add an automated check for it.
From then on, every new decision gets an ADR, and you can ask the agent to draft it.
Conclusion
Documenting decisions has always been good practice. The difference now is who reads them. With AI agents on the team, your ADRs are read every day, by a reader that has no memory, never asks "why", and will happily change anything that isn't explained.
Write the "why" down, turn it into rules, and enforce what matters most. That's what makes an agent's work fit your project instead of a generic idea of what a project should look like.
Do you use ADRs in your projects? How do you give that context to your agents? I'd like to hear what has worked for you.
Top comments (0)