Stackness
Sign up
Architecture file for coding agents: what to put in it, what it costs in context, and a CI check that keeps it true

Architecture file for coding agents: what to put in it, what it costs in context, and a CI check that keeps it true

This playbook builds an architecture file for coding agents in three parts: a 33-line map of who owns what, how a request flows and which patterns are legacy, which loads into every session for 603 tokens; per-area notes that load only when the agent touches that area; and a 21-line script that fails CI when either file names a path, function or glob the code no longer has. I tested every step on the Stackness backend on 25 October 2026, with Claude Code 2.1.310, git 2.50.1 and the Claude token counting API.

What you need

  • A repository with a coding agent: Claude Code, Codex CLI or Cursor.
  • bash, git and jq. The check runs on macOS's stock bash 3.2.
  • An Anthropic API key for step 3. Token counting is free but rate-limited.

What is an architecture file for coding agents?

An architecture file for coding agents is a short, hand-written note that tells an agent what the code cannot: which module owns which job, which way data flows, and which of two patterns in the repository is the current one. It sits next to AGENTS.md, which holds commands and conventions, and is a different thing from a memory plugin, which replays what past sessions learned.

The idea is old: Aleksey Kladov's ARCHITECTURE.md post dates from 2021. The cold-start version came back on 19 October 2026 in a Reddit thread where the poster wrote that, before the files, "the agent poking around to figure out how the services fit together" often guessed wrong.

One finding decides what goes in. The ETH Zurich study of context files (v3, September 2026) measured the steps an agent takes before it touches a file the fix needed. Eight of its twelve developer-written context files had an overview section, and the files "do not meaningfully reduce this metric": "context files are not effective at providing a repository overview." Claude Code's own /doctor proposes cutting "architecture overviews" from CLAUDE.md and keeping "pitfalls, rationale, and conventions". So skip the directory tour. Write ownership, legacy rules and the things the agent must never do.

1. Write the map: ownership, request flow, current versus legacy

The architecture map is one file of 30 to 40 lines that every session loads: an ownership table, one line on how a request flows, the current and legacy version of each pattern that has two, and a short list of things the agent must never do. This is the one I wrote for the Stackness Go backend, ARCHITECTURE.md at the repository root:

# Architecture map

Backend is one Go module in `backend/`. Three binaries share `backend/internal/`.

## Who owns what

| Area | Owner | Entry point |
|---|---|---|
| HTTP API, routes | `backend/cmd/api` | `backend/cmd/api/main.go` |
| Background jobs, schedules | `backend/cmd/worker` | `backend/internal/worker/tasks.go` |
| MCP server | `backend/cmd/mcp` | `backend/internal/mcp` |
| Business rules | `backend/internal/service` | one file per domain |
| Database queries | `backend/internal/repository` | Ent, never raw SQL in services |
| Schema | `backend/ent/schema` | migrations in `backend/migrations` |
| Cache | `backend/internal/cache` | Redis, keys in `backend/internal/cache/keys.go` |

## How a request flows

Router in `backend/cmd/api/main.go` -> handler in `backend/internal/handler` (validates input) -> service (rules, auth checks) -> repository (Ent) -> Postgres. Anything slow is enqueued as an Asynq task and handled by the worker.

## Current vs legacy

- Current: outbound email goes through `service.NewEmailSender`. Legacy: building a Resend client directly.
- Current: task type `email:notification_digest`. Legacy: `email:weekly_digest`, kept only so queued tasks drain. Never enqueue it.

## Never

- Never query Postgres from a handler or a service. Go through `backend/internal/repository`.
- Never send email except through `service.NewEmailSender`.

## Deeper notes

Per-area notes in `.claude/rules/` load when the agent touches that area: background jobs, email.

The legacy section earns its place first. One reply in the thread: "the legacy line saved the most time, before that it kept copying the old code because there's more of it." Name paths and functions in backticks; step 4 checks every one of them.

To load the map, add @ARCHITECTURE.md to CLAUDE.md. Imports load at launch at full cost, the docs note. Codex and Cursor do not inline @file references, so if you use more than one agent, make the map a section of AGENTS.md and give Claude Code a one-line CLAUDE.md that says @AGENTS.md.

2. Split the detail into area notes that load on a trigger

Keep the map short and put each area's detail in its own file that loads when the agent works in that area. The Reddit replies converged on this ("small per-area files work way better for me than one giant doc"), and the trigger matters more than the file count.

In Vercel's eval, a skill the model had to decide to load "was never invoked" in 56% of cases and scored 53%, the same as no docs at all. A path trigger takes that decision away.

My background jobs note is a Claude Code path-scoped rule, .claude/rules/arch-background-jobs.md, which loads the first time the agent reads or edits a matching file. Shortened here; the full file is 23 lines:

---
paths:
  - "backend/internal/worker/**"
  - "backend/cmd/worker/**"
---

# Background jobs

- Task type names live in `backend/internal/worker/tasks.go`. One constant per task; the API enqueues with it and the worker handles it.
- Handlers are registered on the mux and schedules on the Asynq scheduler, both in `backend/cmd/worker/main.go`. Cron strings are UTC.

## Renaming a task type

Keep a handler registered for the old name for one deploy, so tasks queued before the deploy still drain. Example: `TypeNotificationDigestLegacy` (`email:weekly_digest`) points at the same handler as `TypeNotificationDigest`.

The same note in each agent:

Agent Per-area file When it loads
Claude Code .claude/rules/*.md with paths: on the first Read, Write or Edit of a matching file
Codex AGENTS.md in the area's directory at start, for directories from the root down to where Codex runs; 32 KiB cap by default
Cursor .cursor/rules/*.mdc with globs, or a nested AGENTS.md when a matching file is in context; a plain .md in .cursor/rules is ignored

3. Measure what the files load

The map is the only part of an architecture file every session pays for, so count its tokens before committing it, with the model you run, because tokenizers differ by model. The Claude token counting endpoint takes the same body as a message:

jq -n --rawfile doc ARCHITECTURE.md \
  '{model: "claude-sonnet-5-5", messages: [{role: "user", content: $doc}]}' |
curl -s https://api.anthropic.com/v1/messages/count_tokens \
  -H "x-api-key: $ANTHROPIC_API_KEY" -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" -d @- | jq .input_tokens

Expected output for my map: 603. Every file I measured, with the root CLAUDE.md of the same repository for scale:

File Lines Tokens, Claude 5.5 Tokens, Sonnet 4.5 Loads
ARCHITECTURE.md (the map) 33 603 429 every session
arch-background-jobs.md 23 445 321 on a worker file
arch-email.md 15 280 208 on an email file
Stackness root CLAUDE.md 367 14,831 10,431 every session

The counts include about 9 tokens of message wrapping. Two things stand out. The current tokenizer counts about 40% more tokens for the same text than Sonnet 4.5's, more than the "approximately 30 percent" the docs give. And my own CLAUDE.md costs about 25 times the map, so it is the file to trim first.

In money, the map is close to free: 603 tokens at Claude Sonnet 5.5's $2 per million input tokens is about $0.0012 a request, and a twentieth of that when cached. The real cost is behaviour. The ETH study found that developer-written context files raised the cost per task by up to 19%, and LLM-written ones by 23% on average, from the extra steps agents took following them.

4. Fail CI when the file names something that is gone

The drift check reads every architecture file, pulls out each backticked path, each backticked Go symbol and each paths: glob, and fails if the repository no longer has it. I found no open tool that does this: link checkers such as lychee check links and URLs, not paths in code spans, and architecture linters enforce rules in code without reading the docs. Save it as scripts/check-architecture.sh and make it executable with chmod +x:

#!/usr/bin/env bash
# Fails when an architecture file names a path, Go symbol or rule glob the code no longer has.
set -uo pipefail
shopt -s nullglob
docs=(ARCHITECTURE.md .claude/rules/arch-*.md)
stale=0
report() { echo "$1"; stale=$((stale + 1)); }
for doc in "${docs[@]}"; do
  [ -f "$doc" ] || { report "$doc: file is missing"; continue; }
  while IFS= read -r path; do
    [ -e "$path" ] || report "$doc: path $path does not exist"
  done < <(grep -oE '`[^` ]+/[^` ]+`' "$doc" | tr -d '`' | sort -u)
  while IFS= read -r sym; do
    git grep -qw "${sym##*.}" -- '*.go' || report "$doc: symbol $sym not found in any .go file"
  done < <(grep -oE '`([a-z]+\.)?[A-Z][A-Za-z0-9]+`' "$doc" | tr -d '`' | sort -u)
  while IFS= read -r glob; do
    [ -n "$(git ls-files -- ":(glob)$glob" | head -n 1)" ] || report "$doc: paths glob $glob matches no tracked file"
  done < <(sed -n 's/^  - "\(.*\)"$/\1/p' "$doc")
done
echo "$stale stale reference(s) in ${#docs[@]} file(s)"
[ "$stale" -eq 0 ]

On the Stackness repository it checks 31 references (23 paths, 4 symbols, 4 globs) in 0.3 seconds:

0 stale reference(s) in 3 file(s)

To see it fail, I added a row for collections, a feature removed on 27 August 2026, and pointed the email note at a file that does not exist:

ARCHITECTURE.md: path backend/internal/service/collection.go does not exist
ARCHITECTURE.md: symbol service.NewCollectionService not found in any .go file
.claude/rules/arch-email.md: paths glob backend/internal/worker/mailer*.go matches no tracked file
3 stale reference(s) in 3 file(s)

The glob check matters most: a rule whose paths: match nothing never loads, and nothing tells you. A deleted or renamed map fails too, with ARCHITECTURE.md: file is missing. Run the script on every pull request with GitHub Actions:

name: architecture-docs
on: pull_request
jobs:
  drift:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v4
      - run: scripts/check-architecture.sh

Kladov advised naming files but not linking them, because "links go stale". A check that fails the build is what makes naming exact paths safe. It cannot catch a file that still exists but now does something else, so keep a "last checked" date at the top of each file. In CLAUDE.md, put it in an HTML comment: Claude Code strips block-level comments from CLAUDE.md files before injecting them, so the date costs no tokens.

Check it worked

In Claude Code, run /context and check the list under Memory files for the map. Path-scoped rules load later, when a matching file is read; the documented InstructionsLoaded hook logs each load if you want proof. In Codex, run codex --ask-for-approval never "Show which instruction files are active." from the area's directory. Then start a fresh session and ask where a new scheduled task goes; the answer should name the three files from the note without a search first.

What goes wrong

Five failures are common with architecture files: the agent explores anyway, subagents never see the map, area notes vanish after compaction, Cursor skips the rule file, and Codex cuts long instructions. Each has a fix that takes a few minutes.

The agent still explores before editing

That matches the ETH finding: maps do not cut the steps before the first relevant file. Cut the tour and keep ownership, the legacy rules and the never list, which the code cannot tell the agent.

Explore and Plan subagents ignore the map

Claude Code's built-in Explore and Plan subagents start without CLAUDE.md, so they never see the map, though matching path-scoped rules still load for them. If a map rule must reach them, put it in the prompt you give them.

Area notes disappear after compaction

Path-scoped rules and nested CLAUDE.md files are summarised away by compaction and come back only when a matching file is touched again. A rule that must survive goes in the map.

Cursor never applies the rule

Cursor reads only .mdc files in .cursor/rules. Rename the file or move the text into a nested AGENTS.md.

Codex drops the end of the instructions

Codex stops adding AGENTS.md files at 32 KiB by default. Its two doc pages disagree on whether the cap is per file or for all files combined, so keep the total under it.

Checklist

  • Map of 30 to 40 lines: ownership table, one request-flow line, current versus legacy, a never list.
  • No directory tour.
  • One note per area, loaded by a path trigger rather than a model decision.
  • Map counted with the token API; root CLAUDE.md counted too.
  • scripts/check-architecture.sh passing locally and running on every pull request.
  • Last-checked date at the top of each file.

This is the context engineering move applied to architecture, and a narrow case of treating the context window as scarce. For which instruction belongs in which file, see AGENTS.md vs CLAUDE.md vs skills; for the AI tools developers pair with their agents, see the category page.

New posts by email

About one email a week with what we published. No account needed.

One email to confirm first. Every email has a one-click unsubscribe.

Which of these do you use?

Nobody on Stackness runs jq yet. Tick what you use and be the first.

Lonny skating the lakeside path, kickflipping onto a bench, grinding it and flipping offStackness gamesPlay Lonny Eagle Amateur Skater 4A pixel art skateboarding game. Signing up opens it straight away.+2 secret games you unlock as you goSign up to play