Skip to content

Repository Structure for Agents

A human engineer navigates your repo with tribal knowledge: they know the billing logic is "really" in utils/helpers2.ts because Priya mentioned it in standup last March. An agent has none of that. It navigates by exactly two mechanisms — convention (predictable names in predictable places) and grep (finding strings that match what it's looking for). Every deviation from boring, guessable structure is invisible to you and expensive to the agent: it burns context window on exploration, or worse, doesn't find the thing and reimplements it.

This inverts a common instinct. On human-only teams, structure debt is paid slowly, in onboarding time. With agents, it's paid on every single task, because every session starts with zero memory. Repo structure is now agent-performance tuning.

Not every repo needs all of this. But every fact should have exactly one home, and these are the homes agents (and Claude Code specifically) look for first:

.
├── CLAUDE.md               # Agent instructions — Claude Code loads at session start
├── AGENTS.md               # Cross-tool equivalent (or symlink/pointer to CLAUDE.md)
├── README.md               # Human entry point: what this is, how to run it
├── TASKS.md                # Work state: in-progress, blocked, next — survives sessions
├── CHANGELOG.md            # What shipped, when, and why it mattered
├── docs/
│   ├── ARCHITECTURE.md     # System shape, boundaries, invariants
│   ├── specs/              # Product/feature specs — one file per feature
│   │   ├── billing-proration.md
│   │   └── csv-export.md
│   ├── decisions/          # ADRs — numbered, immutable once accepted
│   │   ├── 0001-postgres-over-dynamo.md
│   │   └── 0002-no-orm-in-payment-path.md
│   └── runbooks/           # Operational procedures (deploy, rollback, oncall)
├── packages/               # (monorepo) each package gets its own CLAUDE.md
│   └── billing/
│       ├── CLAUDE.md       # Package-specific rules only — inherits root
│       └── src/
├── src/
├── tests/
└── .claude/
    ├── commands/           # Slash commands — repeatable workflows as prompts
    └── settings.json       # Hooks, permissions, tool config

Three placements matter more than the rest:

  • docs/specs/ — specs live in the repo, not in Notion/Confluence. The agent can read docs/specs/billing-proration.md; it cannot read your wiki. See Keeping Specs Alive.
  • docs/decisions/ — ADRs stop agents (and new humans) from relitigating settled questions. "Why not just use an ORM here?" is answered by 0002-no-orm-in-payment-path.md, not by you, again, in chat. See Decision Records.
  • CLAUDE.md at root + per-package — the root file carries repo-wide rules; package files carry only the delta ("this package targets Node 18, not 22; tests use vitest, not jest"). Claude Code merges them by directory. Full treatment in CLAUDE.md and AGENTS.md.

The repository as shared memory

Each document exists because a specific agent activity reads it. If a doc has no consuming activity, it's decoration; if an activity has no feeding doc, the agent invents the missing input.

The two return edges are the ones teams forget: coding updates TASKS.md, and review findings that repeat become CLAUDE.md rules. Memory that only flows outward goes stale. Docs as Memory covers the maintenance loop.

Principles

One obvious place per fact. If deploy steps live in README.md, docs/runbooks/deploy.md, and a wiki page, the agent will read one of them — a coin flip weighted toward the stale one. Pick a home, and make the other locations one-line pointers.

Boring, predictable naming. docs/specs/csv-export.md is findable by an agent that has never seen your repo, because it would guess that path. docs/planning/2024/Q3/exports_v2_final.md is findable only by grep luck. Optimize for guessability: an agent told "implement the CSV export spec" should locate the file without searching.

Colocate contracts with code. The OpenAPI spec lives next to the routes it describes; the schema migration lives next to the model; the package's constraints live in the package's CLAUDE.md. Agents read locally first — when they open packages/billing/, everything governing billing should be reachable within that subtree or explicitly pointed to from it.

Keep files under ~500 lines. Agents read files in chunks. A 3,000-line services/api.ts means the agent reads the first chunk, forms a wrong model of the file, and edits accordingly. Files under ~500 lines fit in a single read and a single coherent mental model. This applies to docs doubly: a 500-line ARCHITECTURE.md gets skimmed; five 80-line focused docs get read.

Structure encodes boundaries. If packages/billing/ must never import from packages/admin/, say so in CLAUDE.md and enforce it with a lint rule — agents respect visible structure and stated rules, but only tooling catches the exceptions. Unenforced boundaries erode a few hundred lines at a time; see Architecture Erosion.

Anti-patterns

Anti-patternWhat the agent does with itFix
utils/, helpers/, misc/ dumping groundsCan't guess what's inside; reimplements existing functions with new namesSplit by domain (lib/retry.ts, lib/currency.ts); index them in CLAUDE.md per Navigability
Specs in Notion/Confluence/Google DocsNever reads them; invents requirements confidentlyMove to docs/specs/; wiki keeps a link, repo keeps the truth
Same fact in three files, two staleReads whichever it finds first; ships behavior matching the 2023 versionOne canonical home; other mentions become links
2,000-line "god files"Partial reads → wrong mental model → edits that break the part it didn't readSplit under ~500 lines along seams the file already has
Clever/ironic names (ninja.ts, wat.py, TheManager)Grep for "retry" or "auth" finds nothing; duplicates the logicRename to what it does; agents search by literal meaning
Dead code left "for reference"Treats it as a live pattern and imitates itDelete it; Git is the reference. See Navigability
Config sprayed across .env, config/, hardcoded constantsChanges one of four places; bug shipsSingle config module; CLAUDE.md names it as the only place
Empty scaffold dirs from a template (middlewares/, interceptors/)Infers you use those patterns and starts filling them inDelete unused scaffolding; empty structure is a false signal

When you can't restructure

Legacy repos rarely earn a big-bang reorganization. The compensating move is documentation: a CLAUDE.md section titled "Where things actually live" that maps concepts to their real, ugly locations ("rate limiting: src/middleware/throttle_v2.js — ignore throttle.js, it's dead"). Ten lines of honest map beat a month of file-moving, and every agent session benefits immediately. Then fix structure opportunistically, one PR at a time, updating the map as you go.

A field manual for AI-native software engineering.