Architecture·9 min read

Structure a project an agent understands

An AI agent reads your codebase the way a new hire does on their first day — except it can't grab coffee with a teammate to ask "wait, where does auth live?" The structure IS the onboarding. This is a playbook for shaping a project so an agent (and future-you) can navigate it, change it safely, and not hallucinate its way into a mess.

An agent navigates by names and locality, not by reading everything

Agents don't load your whole repo into their head. They grep, they open a few files, they pattern-match on names, and they infer the rest. That means your folder and file names are load-bearing — they're the search index. A file called `utils.ts` with 40 unrelated functions is a black hole: the agent can't guess what's inside without reading all of it, and it'll often just reimplement something that already exists three lines away. Name by capability, not by category. `sendPasswordResetEmail.ts` tells the agent exactly when to open it; `helpers.ts` tells it nothing. The single highest-leverage habit is colocation: keep the code that changes together in the same folder, so a feature is a directory an agent can read top-to-bottom, not a scavenger hunt across `/components`, `/hooks`, `/lib`, and `/types`.

text
# Agent-hostile: split by technical layer
src/components/CheckoutForm.tsx
src/hooks/useCheckout.ts
src/lib/checkout-utils.ts
src/types/checkout.ts

# Agent-friendly: colocate by feature
src/features/checkout/
  CheckoutForm.tsx
  useCheckout.ts
  checkout.server.ts   # server-only logic
  checkout.types.ts
  checkout.test.ts

CLAUDE.md is the map you hand the agent before it walks in

Every serious agent-oriented repo has a `CLAUDE.md` (or `AGENTS.md`) at the root. This is not documentation for humans — it's a briefing for a stateless collaborator who forgets everything between sessions. It should answer the questions an agent wastes tokens rediscovering every time: how do I run the app, how do I run tests, what package manager, where does the important stuff live, and what are the three rules I'll get wrong if you don't tell me. Keep it short and specific. A 2,000-line CLAUDE.md is as useless as none, because the agent skims it. The best ones read like a terse runbook, not an essay.

markdown
# CLAUDE.md

## Commands
- Install: `pnpm install`
- Dev: `pnpm dev` (Next.js, port 3000)
- Test: `pnpm test` (Vitest) — run before claiming done
- Typecheck: `pnpm typecheck`

## Where things live
- Features: `src/features/*` (colocated)
- DB: Drizzle schema in `src/db/schema.ts`
- Auth: Clerk — never hand-roll sessions

## Rules
- Server code goes in `*.server.ts`. Never import it into client components.
- Money is stored in integer cents, never floats.
- All new routes need a test.

Enforce boundaries with types and structure, not with hope

An agent will happily import your database client into a React component if nothing stops it — the code often even runs in dev before exploding in production. Structure that only exists as a convention in your head is invisible to the agent. Make the boundary physical and mechanical. Use a `.server.ts` / `.client.ts` naming convention that your framework or a lint rule actually enforces. Put a real module boundary between your domain logic and your framework so the agent can see where the seams are. The goal is that the wrong thing is hard to type, not merely discouraged in a comment the agent didn't read. Tools like ESLint's `no-restricted-imports`, or a monorepo boundary rule, turn 'please don't' into a red squiggle the agent will fix on its own.

js
// eslint.config.js — makes the boundary real
{
  rules: {
    'no-restricted-imports': ['error', {
      patterns: [{
        group: ['**/*.server'],
        message: 'Server modules can only be imported by other server code.'
      }]
    }]
  }
}

A tight, fast feedback loop is worth more than any doc

The single biggest multiplier on agent quality is whether it can check its own work in seconds. An agent that can run `pnpm typecheck` and `pnpm test` and see a green result will self-correct through several bad attempts and hand you working code. An agent with no runnable feedback loop is writing code blind and you become the compiler. So invest in the boring infrastructure: a `typecheck` script, fast unit tests (Vitest, not a 10-minute suite), and at least one end-to-end smoke test with Playwright for the critical path. Strict TypeScript is doing real work here — every `any` you allow is a place the agent's mistake goes undetected until runtime. The payoff is that you can trust 'the tests pass' as a real signal instead of re-reviewing every line.

One idiom per job — pick it, write it down, and let the pattern propagate

Agents are pattern-matchers by nature. Show them a codebase where data fetching is done three different ways and they'll invent a fourth. Show them one canonical example and they'll clone it faithfully. This is a superpower if you use it deliberately: decide the one way you fetch data, the one way you handle forms, the one way you talk to the database, and make sure the first example in the repo is the good one. The agent treats existing code as the spec. That cuts both ways — a single copy-pasted anti-pattern will metastasize across the codebase in a week of agent work. When you catch the agent doing something the wrong-but-existing way, fix the original too, or you're just teaching it the bad habit again next session. Consistency beats cleverness; a boring, uniform codebase is one an agent can extend without you.

typescript
// The ONE way to load data — every server component copies this shape.
// src/features/orders/orders.server.ts
export async function getOrder(id: string, userId: string) {
  const order = await db.query.orders.findFirst({
    where: and(eq(orders.id, id), eq(orders.userId, userId)),
  });
  if (!order) throw new NotFoundError('order', id);
  return order;
}
// Note the userId scoping is IN the query, not a check after.
// That pattern, repeated everywhere, is how you avoid a whole class of auth bugs.

Make dependencies explicit and shallow — hidden coupling is where agents break things

The changes an agent gets wrong are almost always the ones with invisible ripple effects: it edits a function, tests pass locally, and something three modules away silently breaks because of a dependency it couldn't see. Your job is to make the blast radius of any change legible. Prefer explicit arguments over reaching into global singletons and ambient context. Keep import graphs shallow and directional — features depend on shared primitives, never on each other. If `checkout` imports from `profile` which imports from `checkout`, the agent can't reason about either in isolation and neither can you. A good litmus test: can you delete a feature folder and have the breakage be a clean set of import errors, or does the app mysteriously misbehave? The former is a structure an agent can safely operate in.

Give the agent a scratch space and keep secrets out of its reach

Two practical guardrails that pay off immediately. First, agents generate throwaway files — analysis scripts, test data, one-off migrations. Give that a designated home (a gitignored `/scratch` or `/tmp` dir) so it doesn't litter your source tree, and tell it so in CLAUDE.md. Second, and non-negotiable: never let real secrets live where the agent reads. Keep a committed `.env.example` with the shape of every variable and dummy values, and keep the real `.env` gitignored. The agent needs to know that `STRIPE_SECRET_KEY` and `RESEND_API_KEY` exist and where they're consumed — it does not need the values, and you do not want them in a transcript. This also doubles as documentation: `.env.example` is the canonical list of external services your app talks to, which is exactly the context an agent needs to wire up a new integration correctly.

bash
.env                 # gitignored — real keys, agent never needs these
.env.example         # committed — the contract

# .env.example
DATABASE_URL=postgres://user:pass@localhost:5432/dev
CLERK_SECRET_KEY=sk_test_xxx
STRIPE_SECRET_KEY=sk_test_xxx
RESEND_API_KEY=re_xxx

Structure for reversibility — assume the agent will be wrong sometimes

You are not going to review every line an agent writes as carefully as you'd review your own, and pretending otherwise is how bad code ships. So design the project so mistakes are cheap to catch and cheap to undo. Small, frequent commits mean a bad change is a one-line `git revert`, not an archaeology project. Feature folders with clear boundaries mean a broken feature is quarantined, not entangled. Schema migrations that are versioned and reversible (Drizzle, Prisma Migrate) mean the agent can't silently corrupt your data model. The mindset shift: you're not trying to make the agent never fail — it will. You're structuring the codebase so that when it fails, the failure is loud, local, and reversible instead of quiet, global, and permanent.

Takeaways

  • Colocate code by feature, not by technical layer — a feature should be one folder an agent can read top-to-bottom.
  • Name files by capability (`sendResetEmail.ts`), never by category (`utils.ts`) — names are the agent's search index.
  • A short, runbook-style CLAUDE.md that lists commands, layout, and 3-5 hard rules beats a 2,000-line essay.
  • A fast typecheck-and-test loop lets the agent self-correct; without it, you become the compiler.
  • One canonical pattern per job propagates cleanly — but so does one copy-pasted anti-pattern. Fix the original, not just the copy.
  • Keep `.env` gitignored and commit `.env.example`; the agent needs the shape of your secrets, never the values.