Writing AI Context Files with Information Architecture
Intro
Many of you who have started using AI coding agents in a team have probably run into a similar problem. Every dev has a different favourite: some use Claude Code, some use GitHub Copilot, some use Codex or Gemini CLI, and each one has its own context files: CLAUDE.md, .github/copilot-instructions.md, .claude/rules/, .github/instructions/, .codex/ and plenty more.
From the start, my team decided not to tie ourselves to any single AI vendor. Use whichever one you're comfortable with. What has to be the same is that every agent understands the system the same way, follows the same set of rules, and builds and tests with the same commands.
After a while the context files kept piling up (the repo I look after has almost 300 AI context files if you count every vendor's copy). There are three things I want from this set of files:
- SSOT (Single Source of Truth): each topic has one owner file. If two places disagree, the owner file wins.
- DRY (Don't Repeat Yourself): other files that need the topic point to the owner instead of repeating it. Fix it once and every agent sees the same thing.
- DAG (Directed Acyclic Graph): references go one way. The agent reads from the map down into the details, without looping back to files it has already read.
In this blog I'll walk through how I used the principles of Information Architecture (IA) and Information Hierarchy, which come from the UX and Technical Writing world, to structure these files. I won't go into how each vendor is configured, because that changes far too quickly.
If you only read this part, the takeaway is three points:
- One topic, one owner file (SSOT + DRY). Other files point to it and don't explain it again.
- One-way references (DAG), from the map files down to the detail files. Never pointing back up, and never pointing at each other within the same layer.
- Vendor files are just adapters. There's one source, and a script syncs it out to each vendor.
Pain Point
A real example from my own repo is the readme.md in .claude/skills/, which used to state two rules. The first said we had both .gemini/skills/ and .claude/skills/, but .gemini/skills/ never existed in the repo at all. The second said not to use .github/skills/, on the belief that Copilot reads skills from Claude Code's folder. In fact Copilot reads .github/skills/, .claude/skills/ and .agents/skills/, and .github/skills/ is Copilot's own home folder. Those two rules sat in the repo for months before anyone noticed.
The scary part is that an agent reading rules like these follows them with full confidence, and nothing fails to tell you. When documentation for people is wrong, the reader might still get suspicious. An agent doesn't.
How IA and Information Hierarchy differ
These two terms often get mixed up, but they answer different questions.
| Aspect | Information Architecture | Information Hierarchy |
|---|---|---|
| Key question | Where information lives and how it connects | What matters more and should be seen first |
| Scope | System level, many files, many folders | Inside a single file |
| Analogy | The floor plan of a house | How each room is arranged, with everyday things up front |
IA can be split into four systems: Organization (grouping), Labeling (naming), Navigation (where to go next) and Search (finding things). Map them onto AI context files and they line up almost exactly.
- Organization is the folder structure:
docs/,.agents/instructions/,.agents/skills/ - Labeling is the file names, plus the
descriptionin the frontmatter that the agent uses to decide which file to load - Navigation is the links and tables in
AGENTS.md - Search is the agent grepping on its own, which it already does well if things are named well
If the IA is bad, the agent can't find things. If the Hierarchy is bad, the agent finds them but can't tell what matters.
The principles I borrowed
- Pyramid Principle / BLUF: open with the conclusion, then follow with the reasons and the details. Reading just the first line should already give you the point.
- MECE: split topics so they don't overlap and nothing is missing. This gets you DRY right from the start, because each topic has exactly one home.
- Progressive Disclosure: the top layer is a map and the lower layers hold the detail. The reader chooses how deep to go.
- Chunking: break content into small pieces. If a section has more than 4–5 items, group them.
Why this works for AI (and works even better than it does for people)
LLMs have the same limits as people, just more pronounced.
- Context is an attention budget. The goal is the fewest tokens with the most substance. Repeated text, or text that has nothing to do with the task, dilutes the text that matters.
- Lost in the Middle: models use information at the start and end of the context better than information in the middle, so the important rules should sit at the top of the file.
- Context Rot: the longer the input, the less reliable the output, even for simple tasks.
- Agents load files only when they need them. Agent Skills, for example, load only the
nameanddescriptionat start-up, load the fullSKILL.mdonly when the task matches, and load files inreferences/only when SKILL.md points to them. That's Progressive Disclosure, plain and simple.
As you can see, writing context for AI isn't that different from doing IA for people. The reader is just an agent that reads very fast, never gets suspicious, and forgets everything when the session ends.
The structure: one source, many vendor adapters
The first problem to solve was staying vendor neutral, because every vendor reads files from a different place.
AGENTS.mdis an open standard that many vendors can read- Claude Code mainly reads
CLAUDE.md(newer versions readAGENTS.mdinstead if there's noCLAUDE.md) - Copilot reads
.github/copilot-instructions.mdand.github/instructions/*.instructions.mdwithapplyTo - Claude Code reads
.claude/rules/, which usespathsinstead ofapplyTo
So I split things into a source that is edited in one place, and adapters that must never be edited directly.
AGENTS.md ← L0 map (source)
CLAUDE.md ← adapter: a single line, @AGENTS.md
.github/copilot-instructions.md ← adapter: just says to go read AGENTS.md
.agents/ ← source for skills, instructions, agents
├── instructions/
├── skills/
└── agents/
.claude/rules/, .claude/skills/ ← generated copy (synced from .agents/)
.github/instructions/, skills/ ← generated copy
.codex/agents/*.toml ← generated (converted to TOML)
CLAUDE.md really is just this:
@AGENTS.md
The copies in .claude/ and .github/ are pushed out by a sync script. When it syncs to .claude/rules/, the script also converts applyTo into paths and renames files from <name>.instructions.md to <name>.md. There's also a separate command that only reports which copies have drifted from the source.
Looking at this, some of you might say it's still duplicated. And yes, it is. But it's generated duplication, which still counts as DRY because there's only one place to edit. Think of it like .cs and .dll: nobody treats the .dll as a copy they have to maintain by hand.
DRY for AI context means each topic has one place to edit. Generated copies and one-line pointers still fit within that rule.
Precedence has to live in L0
Another thing I ran into: if two instruction files match the same file and contradict each other, who wins? The answer is that no harness decides for you. When several instruction files are merged into the context, the order isn't guaranteed.
So I wrote the precedence rules in one place, AGENTS.md, because it's the one file that is definitely loaded on every request, and instruction files themselves are not allowed to talk about precedence.
Layers of Context
Once the source and the adapters are separated, the next step is to set up the Hierarchy of the source.
| Layer | Files | Job |
|---|---|---|
| L-1 Tooling | .editorconfig, analyzers, build, tests |
Rules a machine can check |
| L0 Map | AGENTS.md, CONSTITUTION.md |
The map + principles that must never be broken, loaded on every request |
| L1 System | docs/System/ARCHITECTURE.md, ONBOARDING.md |
What the system is, what it's made of, how to set up a machine |
| L2 Detail | .agents/instructions/*, .agents/skills/*, each package's README.md |
Topic-specific detail, loaded when the task matches |
The layer I added myself is L-1 Tooling, which sits below all the documentation. The principle is:
If a tool can check a rule, the rule doesn't belong in prose
For example, the copyright header on every file, or requiring documentation comments on public members, used to be written in the docs. Now they live only in .editorconfig, because an analyzer enforces them far more reliably than prose, and prose that repeats them is just a second source waiting to drift.
Another thing that helps a lot: in L0, use a "to do X, read Y" table instead of explaining, for example:
| Task | Read |
| -------------------- | ------------------------------------------------------- |
| C# code | csharp-code.instructions.md, csharp-docs.instructions.md |
| Database migration | ef-migrations.instructions.md |
| Dockerfile / Compose | docker.instructions.md |
| CI pipeline | pipelines.instructions.md |
This table does the job of both Navigation and Labeling at once. The agent doesn't have to open every file to see which ones are relevant.
SSOT + DRY: one topic, one owner
These two go together. SSOT says where the truth for each topic lives; DRY says other files don't repeat it, they point to the owner instead.
The rules my team actually uses:
- One fact lives in one owner file (SSOT). Any other file that needs it links to it (DRY).
- Rules that apply to the whole set live in the parent file. Child files keep only what is specific to them, because a child file is always read in the context of its parent anyway. A repeated rule gets read twice, and after the first edit the two copies no longer say the same thing.
- A pointer is one line. It names the file and what that file covers, for example "
references/error-messages.md: how the error message is chosen". Once a pointer starts retelling the child file's rules, it has become a copy. - When you move something, delete the old version in the same commit. If you leave it in both places, it's a copy from the second it's merged.
- Don't copy code into docs. Point to the code file instead. Docs should say the why and the scope, and let the code say the how.
The one exception where I allow duplication is the orientation copy, like the Boundaries section in AGENTS.md that says which paths must not be touched. The real detail lives in ARCHITECTURE.md, but it has to be visible from L0 because getting it wrong hurts. I label it right there, "this is the orientation copy", so whoever edits it knows the duplication is on purpose.
DAG: One-way reference
There are two kinds of circular reference.
The first is a link cycle. A says to read B, then B says to go back and read A, so the agent loads a file that is already in its context all over again.
The second is a definition cycle. "Session" is defined using "Token", and "Token" is defined using "Session", so the reader learns nothing new from either definition. The fix is to order the glossary from the most basic terms to the most complex, and only define each term with terms that come before it.
The rest of this section is about the first kind.
Rule: Links only point down
L0 (AGENTS.md) → L1 (docs/System) → L2 (instructions, skills)
- L1 must not point back up to L0
- Files in the same layer must not point at each other
- If every link points down, a cycle simply can't happen
This structure has a name: a DAG (Directed Acyclic Graph). Every link has a direction (directed), and following links never brings you back to a file you already visited (acyclic). It's the same shape as project dependencies or a layered architecture, which developers already know.
In the L2 layer my team goes even further: instruction files must be standalone and must not point at any other file at all. If a rule needs information like a path or a constant, write it into the rule. The only exception is pointing at a skill or a template, because that's an action, not more reading. The reason is that an instruction file is loaded because its applyTo matched, and by then AGENTS.md is already in the context, so pointing back just wastes context.
What I found in the actual repo
Even with this rule in place, when I checked I still found 4 cycles.
docs/System/ARCHITECTURE.md -> CONSTITUTION.md -> docs/System/ARCHITECTURE.md
docs/System/ARCHITECTURE.md -> docs/System/ONBOARDING.md -> docs/System/ARCHITECTURE.md
AGENTS.md -> docs/System/ARCHITECTURE.md -> docs/System/ONBOARDING.md -> AGENTS.md
AGENTS.md -> docs/System/ARCHITECTURE.md -> AGENTS.md
As you can see, every cycle is in the L0/L1 layers, and L2 has none, because the standalone rule is enforced there. The problem sits in the upper-layer files that nobody wrote a rule for.
Going through the links one by one, the causes fall into three groups.
1. A file header that repeats the map. Both ARCHITECTURE.md and ONBOARDING.md open with a sentence along these lines:
How to set up a machine is in [ONBOARDING.md](ONBOARDING.md); how an agent
operates here is in [AGENTS.md](../../AGENTS.md); the binding principles are
in [CONSTITUTION.md](../../CONSTITUTION.md).
This repeats the map in AGENTS.md, which already says ARCHITECTURE.md is what the system is and ONBOARDING.md is how to set up a machine. Every agent comes in through L0 anyway, so the map is always in the context before an L1 file is opened.
The fix: remove the repeated map from L1 and keep only the sentence that says what the file is.
What this repository is: the pattern, the structure, the components, and the
explicit technology choices and limits.
2. Citing a principle instead of stating the fact. For example, the Notes cell for the playground/ row in the Project Structure table of ARCHITECTURE.md:
Writable; never a reference.
See the constitution's Experimental Quarantine.
The second sentence adds nothing, because the fact is already complete in the first one, and the constitution is loaded from L0 anyway.
The fix: state the fact directly and drop the reference, which is exactly the same standalone rule we use for instruction files.
Writable; never used as a reference for new code.
3. Files in the same layer that need each other's information. This is the case that takes the most thought. ONBOARDING.md has to tell someone setting up a new machine which projects don't build, and that list lives in the Known Gaps section of ARCHITECTURE.md, so it has to point across. Meanwhile ARCHITECTURE.md points back to ONBOARDING.md for how to build.
There are two ways out, depending on what kind of content is shared.
Known Gaps is a list that changes often and keeps growing, so I chose to split it into a new file, docs/System/KNOWN-GAPS.md, and have both ARCHITECTURE.md and ONBOARDING.md point down to it instead of pointing at each other.
Note: the file still lives in the same docs/System/ folder, but a layer is defined by what the file does, not by its folder. KNOWN-GAPS.md is a leaf that doesn't point anywhere else, so it counts as L2.
As for the sentence in ONBOARDING.md explaining that AGENTS.md is the main file and CLAUDE.md is only an adapter, that's something L0 already owns, so it can just be deleted.
The result after the fix:
What about people who open an L1 file directly?
This is a trade-off you have to accept. Someone who opens ARCHITECTURE.md straight from Azure DevOps or GitHub won't have a link taking them back up. I chose README.md as the entrance for people and AGENTS.md as the entrance for agents. If you really want a breadcrumb in L1 files you can have one, but it should say where you are, not tell the reader to go read something.
Summary
- L0 is a map.
AGENTS.mdonly says where each topic lives and when to read it. The detail lives in the files below it. - SSOT + DRY. Each topic has one owner file. Other files point to it with a one-line pointer.
- DAG. Links only point down. If files in the same layer need to share something, move it up to the parent or split it down into a child.
- Vendor files are just adapters. Edit
.agents/in one place and let a script sync it out to each vendor. - If a machine can check a rule, let it. Broken links, links that point up, and copies that no longer match the source belong in CI, because a rule that only exists as text will slip sooner or later.
Ref
- AGENTS.md
- Agentic AI Foundation
- Agent Skills specification
- Effective context engineering for AI agents
- Lost in the Middle: How Language Models Use Long Contexts
- Context Rot: How Increasing Input Tokens Impacts LLM Performance
- VS Code: Use custom instructions
- Claude Code reads AGENTS.md
- Information Architecture for the World Wide Web
- The Minto Pyramid Principle
- Diátaxis
- Single source of truth
- Don't repeat yourself
- Directed acyclic graph
Ads
Thanks for reading all the way down here.
Also, if you'd like to follow my work you can follow my personal Github, or hit the yellow button below to cheer me on with some beer and coffee money for the next piece.
Found this useful? You can support the blog with a coffee.
Buy me a coffee