Claude Code reads a file called CLAUDE.md before every interaction. This file is the difference between an AI that writes code you immediately delete and one that writes code that fits your codebase. Most developers either skip it entirely or stuff it with so much text that Claude ignores half of it. Both approaches waste money and produce bad output.
What CLAUDE.md Actually Does
When Claude Code starts a session, it loads CLAUDE.md from the project root into its system context. Every token in that file counts against the context window and costs money. The file is advisory - Claude will follow it roughly 80% of the time, not 100%. This distinction matters and is covered more in the hooks post.
There are three levels of instructions:
| Level | Location | Scope |
|---|---|---|
| Project | ./CLAUDE.md in repo root |
Everyone on the project |
| User | ~/.claude/CLAUDE.md |
All projects for this user |
| Path-specific | .claude/rules/*.md |
Files matching specific patterns |
Project-level instructions ship with the repo. User-level instructions are personal preferences - editor style, commit message format, response verbosity. Path-specific rules fire only when Claude is working on matching files, saving context tokens everywhere else.
What Goes In CLAUDE.md
The effective CLAUDE.md files all share the same structure: build commands, architecture decisions, and hard rules. Nothing else.
Build and Test Commands
## Build
- `npm run build` - production build
- `npm run dev` - dev server on port 3000
- `npm test` - full test suite
- `npm test -- --grep "auth"` - run specific tests
This seems obvious but skipping it means Claude will guess. It will run yarn in an npm project, use pytest when the project uses unittest, or skip tests entirely. Explicit commands eliminate an entire class of wasted iterations.
Architecture Decisions
## Architecture
- Monorepo with pnpm workspaces: packages/api, packages/web, packages/shared
- API uses Express with dependency injection via tsyringe
- Database: PostgreSQL with Drizzle ORM (NOT Prisma - we migrated away)
- Auth: Custom JWT implementation in packages/shared/auth
- All API routes must go through the middleware chain in packages/api/src/middleware/
The critical detail here is the negative instruction: “NOT Prisma.” Claude’s training data is full of Prisma examples. Without this explicit override, it will suggest Prisma patterns in a Drizzle codebase. Negative instructions - what NOT to do - are often more valuable than positive ones.
Hard Rules
## Rules
- NEVER import from packages/web in packages/api
- All database queries go through the repository layer, never direct Drizzle calls in routes
- Error responses use the ApiError class from packages/shared/errors
- Test files are colocated: foo.ts has foo.test.ts in the same directory
- No default exports. Named exports only.
These are the guardrails. Every rule here should be something that, if violated, would fail code review. If it is a preference rather than a requirement, it probably does not belong here.
What Does NOT Go In CLAUDE.md
This is where most developers go wrong. Stuffing CLAUDE.md with hundreds of lines of context produces diminishing returns fast. Each token costs money and competes for attention in the context window.
Do not include:
-
Entire API documentation. Use
@filereferences or let Claude read files on demand. Pasting 500 lines of API docs into CLAUDE.md burns context on every single interaction, even when the task has nothing to do with that API. -
Generic coding standards. “Use meaningful variable names” and “write clean code” are noise. Claude already does this. Only include rules that are specific to this project.
-
Long explanations of why. CLAUDE.md is for the what. “Use Drizzle ORM” is sufficient. A three-paragraph explanation of why the team migrated from Prisma is wasted context.
-
Frequently changing information. If the content changes every sprint, it will be stale more often than it is current. Put volatile context in task-specific prompts instead.
Path-Specific Rules with .claude/rules/
This feature is underused but powerful. Create markdown files in .claude/rules/ with a frontmatter glob pattern:
---
globs: ["packages/api/src/routes/**/*.ts"]
---
Every route handler must:
1. Use the `asyncHandler` wrapper
2. Validate input with zod schemas from the adjacent `.schema.ts` file
3. Return responses using `ApiResponse.success()` or throw `ApiError`
---
globs: ["**/*.test.ts"]
---
Testing conventions:
- Use vitest, not jest
- Use `vi.mock()` for mocking, never manual mocks
- Each test file tests exactly one module
- Use factories from packages/shared/test-utils for test data
These rules only load when Claude is editing files that match the glob. A route-specific rule does not consume context when Claude is writing a migration script. This is free optimization.
Real-World CLAUDE.md Example
Here is a complete, production-tested CLAUDE.md for a medium-sized TypeScript project:
# Project: Acme Dashboard
## Commands
- `pnpm dev` - start dev server
- `pnpm build` - production build
- `pnpm test` - run vitest
- `pnpm lint` - eslint + prettier check
- `pnpm db:migrate` - run Drizzle migrations
- `pnpm db:generate` - generate Drizzle schema from SQL
## Stack
- Next.js 15 App Router (NOT Pages Router)
- Drizzle ORM with PostgreSQL
- TailwindCSS v4
- vitest for testing
- pnpm (not npm, not yarn)
## Architecture
- `src/app/` - Next.js app router pages
- `src/lib/` - shared utilities, DB client, auth
- `src/components/` - React components, colocated with styles
- `src/server/` - server-only code, API routes, DB queries
## Rules
- Server components by default. Add 'use client' only when needed.
- All DB access goes through functions in `src/server/db/queries/`
- Use `cn()` from `src/lib/utils` for conditional classnames
- No barrel exports (index.ts re-exports)
- Dates use date-fns, never raw Date manipulation
- All API responses use the Result type from `src/lib/result`
That is 30 lines. It covers everything Claude needs to write code that passes review in this codebase. Compare this to the 200-line CLAUDE.md files that try to be a complete onboarding document - those cost 5x more per interaction and perform worse because critical rules get buried in noise.
Common Mistakes
Too verbose. Every line costs tokens. A CLAUDE.md over 50 lines should be audited. Move specialized rules to .claude/rules/ with glob patterns.
Conflicting instructions. “Always use async/await” in CLAUDE.md and “Use callbacks for Redis operations” in a rules file will produce inconsistent output. Audit for contradictions.
Duplicating what Claude already knows. “Use TypeScript generics for type-safe functions” is a waste of a line. Claude knows TypeScript. Tell it what is specific to this project.
Forgetting to update. A stale CLAUDE.md that references a deleted directory or a deprecated package is worse than no CLAUDE.md. Treat it like code - review it in PRs.
The Payoff
A well-written CLAUDE.md turns Claude Code from a generic coding assistant into something that understands the project. The first interaction in a session should produce code that looks like a team member wrote it - correct imports, correct patterns, correct test structure. If it does not, the CLAUDE.md needs work. Iterate on it the same way the codebase itself gets iterated on. The compound returns are significant.
Comments