Project structure optimized for AI coding agents
PLA project structure optimized for AI coding agents groups code by feature, keeps files small and named for what they contain, places each test beside the code it checks, and puts scoped instructions in the directory they govern. Claude Code, Codex and Cursor then find the right files with fewer searches and load only the rules that apply.
You ask the agent to add a refund endpoint. It searches for validate, opens a 1,400-line utils.ts, reads 40 unrelated helpers, picks the wrong one, and writes the new test into a __tests__ tree that CI does not run. The model did what the repository allowed. This page is for developers who restructure code with agents and for tech leads who set the layout conventions every agent on the team inherits.
What you’ll get from restructuring for agents
Section titled “What you’ll get from restructuring for agents”- Five layout rules, each tied to a specific way agents find and read code.
- Directory-scoped instructions for Claude Code, Codex and Cursor, with the loading rule each tool actually follows.
- Four copy-paste prompts: a read-only structure audit, a behaviour-preserving file split, scoped instructions for your busiest directories, and a findability probe.
- CI checks that keep the structure from drifting back: a file-size budget, a circular-import check, and a rename-only diff.
This page assumes you know how agents search a repository. If not, read code search and indexing for AI agents first.
Why does file layout change what the agent reads?
Section titled “Why does file layout change what the agent reads?”An agent learns your codebase the way a new hire does on day one: it lists directories, searches for names, and opens files. Every file it opens lands in the context window, where it competes with your instructions for attention (see how context windows fill up). The layout decides how many of those reads are wasted.
| Layout property | What the agent does with it | What goes wrong without it |
|---|---|---|
| Feature folders | Lists one directory and sees the whole feature | Opens controllers/, services/, models/ and types/ to assemble one change |
| Small files | Reads a file whole and understands it | Reads a partial view of a large file, or reads 1,000 lines to use 20 |
| Descriptive names | Finds the file from a filename search alone | Opens helpers.ts, misc.ts and common.ts speculatively |
| Co-located tests | Derives the test path from the source path | Writes tests in the wrong tree, or skips them |
| Scoped instructions | Loads the billing rules only when it touches billing | Carries every team’s rules in every session, or none |
Large files have one more cost in Claude Code. When a whole-file read exceeds the Read tool’s token limit, the tool returns only the first page with a PARTIAL view notice, and Claude has to page through the rest (Claude Code tools reference, checked 2026-09-26). Claude can miss a helper defined on line 1,300 of such a file.
Which layout rules matter most for agents?
Section titled “Which layout rules matter most for agents?”Apply these in order. The first two give most of the benefit, and the last three depend on them.
Group code by feature, not by file type
Section titled “Group code by feature, not by file type”A type-first layout scatters one feature across the tree. A feature-first layout keeps it in one directory that a single listing reveals:
# Type-first: one refund change touches four directoriessrc/controllers/refund-controller.tssrc/services/refund-service.tssrc/models/refund.tstests/services/refund-service.test.ts
# Feature-first: one directory, one listingsrc/features/refunds/ refund-handler.ts refund-handler.test.ts refund-policy.ts refund-policy.test.ts refund-types.ts AGENTS.md # rules for this feature only CLAUDE.md # one line, @AGENTS.md, for Claude CodeKeep code that really is shared (database client, logging, error types) in src/shared/, split into small files with specific names. shared/ is not a new utils.ts: if a function has one caller, it belongs in that caller’s feature folder.
Keep files small enough to read whole
Section titled “Keep files small enough to read whole”No published study sets the right line count, so choose a budget, write it down and let a linter hold it. Start at 400 lines, the value in the config below, and adjust it once you have seen the audit. What matters more than the number is that the split follows responsibility: refund-policy.ts and refund-handler.ts, never refund-1.ts and refund-2.ts.
Enforce it for TypeScript and JavaScript with ESLint’s core max-lines rule, as a warning first and an error once the backlog is cleared:
export default [ { files: ['src/**/*.{ts,tsx}'], rules: { 'max-lines': ['warn', { max: 400, skipBlankLines: true, skipComments: true }], }, },];Name files for what they contain
Section titled “Name files for what they contain”The filename is the search index. An agent that searches for password should get password-hashing.ts, not a hit on line 612 of auth-helpers.ts.
| Vague | Specific |
|---|---|
utils.ts, helpers.ts, misc.ts | password-hashing.ts, currency-format.ts, csv-parse.ts |
service.ts inside notifications/ | notification-sender.ts |
types.ts with 500 lines | refund-types.ts, invoice-types.ts |
index.ts that re-exports 60 symbols | Direct imports, or a barrel under 20 lines |
Put each test beside the code it checks
Section titled “Put each test beside the code it checks”A fixed rule, x.ts → x.test.ts in the same directory, lets the agent find the test for any file without a search and put new tests where your test runner looks. If your framework needs a separate test tree (for example, Python projects with tests/ mirroring the package), mirror the source path exactly and write the rule into your instruction file.
Keep barrels thin and imports acyclic
Section titled “Keep barrels thin and imports acyclic”A barrel file (index.ts) that re-exports a whole package makes the agent read one file to find the next. Circular imports make module boundaries ambiguous, so the agent cannot tell which module owns a function. Both show up in the CI checks later on this page. For layer rules (UI must not import the database client), use the boundary checks in making a codebase agent-ready rather than a second tool.
Put scoped instructions where the code lives
Section titled “Put scoped instructions where the code lives”The root instruction file should hold what applies everywhere. Rules that apply to one directory belong in that directory, so they load when the agent works there and cost nothing when it does not. The three tools load scoped instructions differently, and the difference decides where you start the session.
Claude Code loads CLAUDE.md files in your working directory and every directory above it at launch. A CLAUDE.md in a subdirectory loads on demand, when Claude reads files in that subdirectory. For rules that follow a file pattern rather than a directory, use a path-scoped rule in .claude/rules/:
---paths: - "src/features/billing/**"---
# Billing rules- Money is integer cents: use the `Cents` type from src/features/billing/money.ts. Never use floats.- Every handler in this folder has a sibling `*.test.ts`. Add one with every new handler.Save it as .claude/rules/billing.md. Path-scoped rules trigger when Claude reads a matching file, not on every tool use (Claude Code memory docs, checked 2026-09-26). Claude Code also reads AGENTS.md in a project with no CLAUDE.md, including AGENTS.md files in subdirectories, as long as there is no CLAUDE.md in the working directory or any directory above it; that needs v2.1.277 or later (v2.1.281 on Bedrock, Google Cloud, Foundry, LLM gateways or with telemetry off), on both release channels since stable reached v2.1.287 (checked 2026-10-12).
Two gotchas. The built-in Explore and Plan subagents skip CLAUDE.md files, so a directory map kept only there does not reach them. In a monorepo, ancestor CLAUDE.md files from other teams still load; exclude them with claudeMdExcludes in .claude/settings.local.json.
Codex finds the project root by walking up from the working directory to the first .git (the default project_root_markers), then concatenates every AGENTS.md from the root down to the working directory. In each directory, an AGENTS.override.md is used instead of AGENTS.md. The total is capped by project_doc_max_bytes, 32,768 bytes by default (codex-rs/core/src/agents_md.rs and config.schema.json at rust-v0.157.1).
That means an AGENTS.md below your working directory is not loaded at start. To get a package’s rules loaded, start Codex in that package:
# terminal: loads the root AGENTS.md and src/features/billing/AGENTS.mdcodex -C src/features/billingCodex’s bundled model instructions tell the model to look for AGENTS.md files when it works below the working directory, but that is the model following an instruction, not the loader guaranteeing it. Since 0.150.0, untrusted projects supply no project-level AGENTS.md at all, so trust the project first.
Codex reads a plain AGENTS.md with no paths frontmatter. If your team also uses Claude Code, keep one copy of the billing rules in this AGENTS.md rather than a second copy in .claude/rules/; the next section shows how.
Cursor calls these project rules. The agent runtime Cursor ships in its @cursor/sdk 1.0.32 package loads .cursor/rules/**/*.mdc files and reads alwaysApply, globs and description from their frontmatter; in Cursor’s rule model these select always-on, file-matched and agent-decided rules respectively (confirm in the editor, see below). Cursor project rules covers the current rule types.
---description: "Billing rules: money types and test placement"globs: "src/features/billing/**"alwaysApply: false---
- Money is integer cents: use the `Cents` type from src/features/billing/money.ts. Never use floats.- Every handler in this folder has a sibling `*.test.ts`. Add one with every new handler.Save it as .cursor/rules/billing.mdc. The frontmatter fields alwaysApply, globs and description decide when a rule applies (Cursor docs, checked 2026-10-12); confirm it in the editor against project rules in Cursor, which also has a canary check that proves a rule loaded.
One file for Claude Code and Codex
Section titled “One file for Claude Code and Codex”A team that uses both tools should write each directory’s rules once, in that directory’s AGENTS.md. Two copies of the same rules drift apart. Make Claude Code see the file in one of two ways (Claude Code memory docs, checked 2026-09-26):
- Add a
CLAUDE.mdbeside it that contains only the import line@AGENTS.md. Imports are expanded when theCLAUDE.mdloads. - Set Project instructions to
claude-md-and-agents-mdin/config, so Claude Code readsCLAUDE.mdandAGENTS.mdfiles together.
Do not rely on the default. With the default setting, claude-md-or-agents-md, Claude Code reads AGENTS.md files only when there is no CLAUDE.md, .claude/CLAUDE.md or CLAUDE.local.md in the working directory or any directory above it. A repository with a root CLAUDE.md therefore makes a bare src/features/billing/AGENTS.md invisible to Claude Code, while Codex still loads it.
Keep each scoped file to rules the agent cannot infer from the code: invariants, the one approved helper, where tests go. Documentation as context covers what else belongs in instruction files, and pruning CLAUDE.md and AGENTS.md covers removing what no longer earns its tokens.
Restructure an existing codebase with an agent
Section titled “Restructure an existing codebase with an agent”A restructure is a mechanical, behaviour-preserving change, which is the kind of change an agent does well and a gate can check fully. Run it in small pull requests, one directory or one catch-all file at a time, so parallel branches do not collide with a repository-wide move.
-
Audit before you move anything. Run the audit prompt below in a fresh session, read-only. Ask for measured line counts, not estimates, and rank the findings by how often each file changes, because a large file nobody touches costs little.
-
Approve the target layout, not the diff. The agent proposes the new tree and the name of every new file. You or the tech lead review that list: it is short, and names are where a bad split shows first.
-
Move one unit per pull request. Use the split prompt. It forbids body changes, so the diff is moves and import updates only.
-
Prove nothing changed. Run the gates in the next section. A pure move needs no line-by-line review when the gates pass.
-
Write the scoped instructions. Once a feature folder exists, add its
CLAUDE.md,AGENTS.mdor.mdcrule with the scoped-instructions prompt. -
Turn the budget into an error. When the audit’s backlog is empty, switch
max-linesfromwarntoerrorso the structure cannot regress.
How do you prove a restructure changed nothing?
Section titled “How do you prove a restructure changed nothing?”You do not read a 60-file move line by line. You make the move checkable and let the checks sign off:
- Rename-only diff.
git diff -M --summary main...HEADlists each moved file as arenamewith a similarity percentage. A pure move shows high similarity on every file; a low percentage marks a file where the agent changed more than imports. A split such asutils.tsinto modules shows up ascreate modeanddelete modeinstead of renames, so for a split rungit diff --color-moved=zebra --color-moved-ws=allow-indentation-change main...HEAD: moved blocks render in the moved colours, and any line in the normal add or delete colour is a real change to review. - Same tests, same results. The type checker, linter and full test suite pass, and the test count is unchanged. A falling count means tests were lost in the move.
- No new cycles.
npx madge --circular --extensions ts,tsx src/exits with code 1 when it finds a circular import (madge 8.0.0), so it works as a CI step as written. - The budget holds.
max-linesruns in the same lint job as everything else. - Findability improved. Run the probe below in a fresh session before and after, and compare how many files the agent opened and how much context it used (
/contextin Claude Code,/statusin Codex).
Run the probe with three questions your team asks often. If the after-run opens as many files as the before-run, the new names are not doing their job. The tech lead owns the layout conventions and approves the target tree in step 2; CI owns everything after that, and a pull request that fails a gate does not merge.
What breaks when you restructure for agents?
Section titled “What breaks when you restructure for agents?”A scoped instruction file never loads. Codex was started at the repository root, so the package AGENTS.md below it was never read; or a Claude Code path rule did not fire because Claude wrote a new file without reading a matching one. Recovery: add a canary line to the scoped file (“When you use a rule from this file, say BILLING-RULES”), start a session the way your team does, and ask a billing question. Start Codex in the package with -C, or ask Claude to read a sibling file first.
Framework conventions break silently. File-based routers, migration folders, plugin discovery and dynamic import() with string paths depend on names and locations the type checker does not see. Recovery: list those directories as off-limits in the split prompt, search for the old path as a string after the move, and run the end-to-end suite, not only unit tests.
The split follows the line budget instead of responsibility. The agent cuts a 900-line file into three files by position, and every change now touches all three. Recovery: reject the file list in step 2 when names are numbered or generic, and ask for a split by responsibility with one sentence per file on what it owns.
A barrel quietly replaces the old catch-all. shared/index.ts re-exports everything, and the agent now reads the barrel plus every module. Recovery: delete the barrel, switch to direct imports, and rerun the audit prompt, whose step 3 flags any index file over 20 lines.
A repository-wide move collides with open branches. Every open pull request conflicts at once. Recovery: move one directory per pull request, merge each within a day, and announce moves before they land.
Generated code sits inside feature folders. The agent searches it, edits it, and the next codegen run erases the change. Recovery: move generated output to a clearly named directory and exclude it from agent search, as in code search and indexing for AI agents.