← Back to Blog

Every Claude Code user eventually writes a CLAUDE.md. Most write it wrong the first time — not because they’re careless, but because nobody handed them a structure before they started filling the file with whatever Claude seemed to be forgetting.

This post gives you the structure. A five-section template, a full production example at 180 lines (counting blank lines and comments), a list of things that actively hurt CLAUDE.md quality, a before/after case study with token counts, a migration flow for extracting skills, and a quarterly maintenance ritual.

If you want to understand why long CLAUDE.md files make drift worse before diving into the template, read The 3000-line CLAUDE.md Problem first. This post is the solution companion — prescriptive, not diagnostic.

The five-section template

A well-structured CLAUDE.md contains exactly five sections. Not four, not eight. Five. Each one has a distinct job. If a sentence doesn’t fit cleanly into one of these five, it probably does not belong in CLAUDE.md at all.

1. Project overview

Three to five lines. What this codebase is, what it does, and who uses it. Write it as if you’re onboarding a contractor for their first hour. No mission statements. No adjectives about quality. Just facts.

Good: “Acme Time Tracking is a SaaS app for freelancers and small agencies. Users log time against projects, get weekly reports by email, and pay via Stripe subscription.”

Bad: “Acme is building the future of work by empowering teams with best-in-class time intelligence solutions.”

2. Stack and deploy target

Bulleted, factual. Framework, database, auth, payments, hosting, CI. Include versions where drift between versions matters (Node, Next.js, Postgres). This is the section Claude uses to avoid writing code for the wrong runtime.

3. Conventions

Project-specific patterns that differ from framework defaults. Naming conventions, test file locations, commit message format, env variable naming scheme, import path aliases. Do not restate things that are already obvious from the framework (e.g., “use TypeScript” when the entire codebase is TypeScript). Only write conventions that a competent developer would not guess on their own.

4. Gotchas

This is the most valuable section in any CLAUDE.md. Five to fifteen specific traps this project has already hit. Not general advice — real, project-specific landmines. “Supabase RLS is enabled on all tables — never call the service role client from the browser.” “The Stripe webhook handler requires the raw body, not the parsed JSON body.” “Resend API key is in RESEND_API_KEY, not EMAIL_API_KEY.”

These gotchas are what make your CLAUDE.md irreplaceable. A generic template can give Claude the stack. Only your gotchas section can save it from the bugs your codebase has already written once.

5. Do not touch

An explicit list of paths, files, or patterns Claude should refuse to modify without explicit user confirmation. Migration files. Generated code. Third-party vendor files. Config files that require manual review before change. This section prevents the most expensive mistakes.

Target length: under 200 lines, including blank lines and comments. If you are over 200 lines, you have not finished extracting skills yet.

A full production CLAUDE.md example

Below is a complete, real-structure CLAUDE.md for a fictional SaaS called Acme Time Tracking (Next.js 15, Supabase, Stripe, Resend). This is 180 lines including blank lines, section headers, and comments. Copy it, replace the specifics, delete anything that doesn’t apply to your project.

# CLAUDE.md — Acme Time Tracking
# Keep this file under 200 lines. Reusable instructions go in ~/.claude/skills/.
# Last audited: 2026-04-20

## 1. Project overview

Acme Time Tracking is a B2B SaaS for freelancers and agencies with up to 25 seats.
Users log time against client projects, generate weekly PDF reports, and pay monthly
via Stripe subscriptions (Solo $12/mo, Team $49/mo, Agency $149/mo).

Primary personas: solo freelancers and 2-5-person agencies.
Revenue model: subscription. No free tier. 14-day trial on signup.

## 2. Stack and deploy target

Runtime:       Node 20 (LTS). Do not use Node 22 features.
Framework:     Next.js 15 App Router. Pages Router is dead in this repo.
Database:      Supabase (Postgres 15). Auth: Supabase Auth (email + Google OAuth).
ORM:           Drizzle ORM. Schema in /db/schema.ts. Never write raw SQL outside /db/.
Payments:      Stripe Billing. Subscription objects only. No one-time charges.
Email:         Resend (SDK v3). Templates in /emails/ using React Email.
Storage:       Supabase Storage. Bucket: acme-reports (PDF exports only).
Hosting:       Vercel. Preview deploys on every PR. Production branch: main.
CI:            GitHub Actions. Lint + type-check + unit tests on every push.
Package mgr:   pnpm 9. Never use npm or yarn in this repo.

## 3. Conventions

### File naming
- React components: PascalCase.tsx (e.g., TimeEntryRow.tsx)
- Server actions: camelCase.ts in /app/actions/ (e.g., createProject.ts)
- API routes: /app/api/{resource}/route.ts
- DB queries: /db/queries/{resource}.ts (one file per Drizzle table)

### TypeScript
- Strict mode is on. No `any`. Use `unknown` + type guards.
- All server action return types must be explicit (no inferred `void`).
- Zod schemas live in /lib/schemas/{domain}.ts. Validate at the boundary, not mid-function.

### Testing
- Unit tests: Vitest. Test files colocated: Button.test.tsx beside Button.tsx.
- Integration tests: /tests/integration/. Run against a local Supabase instance.
- No snapshot tests. Behavioral assertions only.
- New DB queries require at least one integration test.

### Commits
- Conventional commits: feat/fix/chore/refactor/test/docs.
- Scope is required: feat(time-entry): add bulk delete.
- No WIP commits to main.

### Environment variables
- Local: .env.local (gitignored).
- Naming: NEXT_PUBLIC_ prefix only for values safe to expose to the browser.
- Supabase service role key is SUPABASE_SERVICE_ROLE_KEY. Never reference it in
  /app/ or /components/. Server actions and /api/ routes only.

### Styling
- Tailwind CSS v4. No CSS Modules. No styled-components.
- Design tokens in tailwind.config.ts. Do not hardcode hex values in className.
- Dark mode via the `dark:` variant. The `dark` class is toggled on <html>.

## 4. Gotchas

Stripe webhook raw body.
The Stripe webhook handler at /app/api/webhooks/stripe/route.ts uses `req.text()`
not `req.json()`. Next.js parses the body automatically in all other routes.
The webhook handler has `export const config = { api: { bodyParser: false } }`.
Never remove this or add json() parsing to that file.

Supabase RLS is on everywhere.
Every table has row-level security enabled. The anon client cannot read or write
without a valid JWT. The service role client bypasses RLS — never expose it
to the browser or include it in client components. If a query fails silently and
returns [], check RLS policies before suspecting the query.

Drizzle migrations are one-way.
Migration files in /db/migrations/ are append-only. Never edit an existing migration
file. Generate new migrations with: pnpm drizzle-kit generate. Apply with:
pnpm drizzle-kit push (local) or the Supabase migration UI (production).

Next.js App Router caching is aggressive.
fetch() calls in Server Components are cached by default. Opt out with
`cache: 'no-store'` for any data that changes per-request (user data, time entries).
revalidatePath() after mutations. If data looks stale, check the cache config first.

Resend API key name.
The key is RESEND_API_KEY. Not EMAIL_API_KEY, not RESEND_KEY. This has caused
three separate broken-email incidents. The SDK initialization is in /lib/email.ts;
do not initialize Resend inline in other files.

PDF export memory limit.
The PDF generation route at /app/api/reports/pdf/route.ts uses Puppeteer.
It is deployed as a Vercel function with 1024 MB memory limit.
Do not add image assets to the PDF template — they push over the limit.
SVG icons only. The budget has been tested; there is ~180 MB of headroom.

Supabase Auth session in Server Components.
Use createServerClient() from @supabase/ssr, not createClient().
The server client reads cookies from the Next.js cookies() API.
The pattern is in /lib/supabase/server.ts. Always import from there, not inline.

Trial expiry logic.
Trial expiry is computed from profiles.trial_ends_at in Postgres, not from Stripe.
The source of truth for access is the database, not the Stripe subscription status.
There is a nightly cron (Vercel Cron, /app/api/cron/check-trials/route.ts) that
syncs expired trials and marks accounts as `status: suspended`.

## 5. Do not touch

These files and directories require explicit confirmation before any modification:

- /db/migrations/          Generated migration files. Never edit. Add new ones only.
- /app/api/webhooks/       Stripe and Supabase webhook handlers. High blast radius.
- /lib/supabase/server.ts  Auth session pattern. Used everywhere. Changes need a sweep.
- /public/legal/           Privacy policy and ToS PDFs. Legal review required.
- tailwind.config.ts       Design token changes affect every component.
- pnpm-lock.yaml           Never edit manually. Regenerate with pnpm install.
- .env.local.example       Do not add real values. Template only.

# End of CLAUDE.md
# Skills live in ~/.claude/skills/. Do not inline reusable instructions above.

That file is 180 lines. It covers every stack decision, naming rule, landmine, and forbidden zone a new Claude Code session needs to operate safely in this codebase. Nothing in it is aspirational. Nothing in it would apply to a different project.

What to never put in CLAUDE.md

These patterns appear in almost every CLAUDE.md that has grown past 500 lines. Each one degrades session quality.

Instruction lists that should be skills. If you have written “When reviewing a PR, check for…” followed by a bulleted list, that is a skill, not a CLAUDE.md instruction. Extract it to ~/.claude/skills/pr-review/SKILL.md. Prose in CLAUDE.md gets re-interpreted each session; a skill file runs the same structure every time. Here is the full explanation of why.
Mission copy and values statements. “We value clean, maintainable, well-tested code that delights our users.” This sentence does nothing. Claude does not produce worse code because you removed it. Remove it.
Corporate jargon. “Leverage synergistic approaches,” “best-in-class solutions,” “holistic review process.” These phrases are not instructions. They are noise that dilutes the real instructions around them.
General programming advice. “Write readable code.” “Add comments where necessary.” “Follow SOLID principles.” Claude already knows this. You are spending context budget on instructions that produce zero marginal improvement.
Duplicated paragraphs. If you have said the same thing in two sections with slightly different wording, you are patching drift with repetition. Repetition does not fix drift; it signals that the instruction should be a skill with an explicit constraint, not ambient prose.

Before / after case study

A real migration from a production codebase (names changed). The original CLAUDE.md was 2,400 lines accumulated over four months. The refactored version is 178 lines.

2,400
Original line count
178
Refactored line count
~18,400
Context tokens (original)
~1,370
Context tokens (refactored)

The 2,400-line version had 312 lines of PR review instructions, 180 lines of “code quality” guidance, 95 lines of mission and values copy, and 47 lines of instructions that directly contradicted each other across sections. What actually contained project-specific information — stack, conventions, gotchas, do-not-touch — was 190 lines. The other 2,210 lines were instructions that belonged in skills, or instructions that belonged nowhere.

After the migration, the maintainer extracted 8 skills (PR review, test coverage audit, migration safety check, security triage, performance smell, README sync, changelog draft, and effort estimate). Each skill runs more consistently than the embedded prose did, because skills have explicit output shapes and “refuses to” constraints that prose does not.

Reported session quality improvement: fewer unprompted scope expansions, fewer ignored constraints, and more reliable output format across consecutive sessions. The 93% token reduction meant more context budget for actual code, not CLAUDE.md.

The skills migration flow

How to extract reusable instructions from your current CLAUDE.md in five steps.

  1. Run the free analyzer first. Paste your CLAUDE.md into the CLAUDE.md Analyzer. It flags instruction-to-context ratio, aspiration prose, jargon density, and recurring patterns. This takes 30 seconds and tells you exactly where to start.
  2. Find the “when you do X” paragraphs. Any sentence starting with “When reviewing,” “When writing tests,” “When fixing bugs,” is a skill trigger. List every one. Those are your skill candidates.
  3. Create the skill file. For each candidate, create ~/.claude/skills/{name}/SKILL.md. The file needs: a name and description in the front matter, an Inputs section, a numbered Steps sequence with explicit constraints, a structured Output format, and a “Refuses to” block listing what the skill will not do. The structure forces specificity that prose never achieves.
  4. Delete the extracted instructions from CLAUDE.md. This is the step most people skip. Leaving the prose in CLAUDE.md after creating the skill gives you two conflicting sources of truth with two different interpretations. Delete the paragraph. The skill is now the source of truth.
  5. Verify each skill in isolation before the next extraction. Invoke the new skill by name. Confirm the output matches the specified format. Then extract the next one. Batch extraction without verification produces skills that all fail quietly.

If you want to skip writing the skills yourself, the Septim Drills pack ships 25 pre-built skills covering the most common patterns — PR review, test audit, migration safety, security triage, and more. Drop the directory into ~/.claude/skills/ and they work immediately.

Maintenance cadence: the quarterly CLAUDE.md audit

A CLAUDE.md written in January is wrong by April. The stack changes. New gotchas appear. Old gotchas get fixed and their entries become noise that dilutes the real ones. Run this audit every quarter.

The 20-minute quarterly ritual

  1. Line count check. If it is over 200 lines, something new was added that should be a skill. Find it and extract it before proceeding.
  2. Gotchas review. For every gotcha, ask: is this trap still live? Has the underlying issue been fixed? Remove resolved gotchas immediately. Stale gotchas are worse than no gotchas — they teach Claude wrong things.
  3. Stack version audit. Update any version numbers that have changed. Outdated stack entries cause Claude to write code for old APIs.
  4. Do-not-touch review. Any paths removed from the list? Any new paths that should be added? This list drifts silently.
  5. Skill directory sweep. Open ~/.claude/skills/. Any skill you have not invoked in 90 days is probably either broken or redundant. Test it or archive it.

Calendar it. The maintainer who skips the quarterly audit is the maintainer who writes “Do not modify the payment logic” in CLAUDE.md at 2am after an incident.

Free: run the CLAUDE.md Analyzer on your file right now

Client-side tool. Your file never leaves your browser. Flags length, jargon density, aspiration prose ratio, and the top skill extraction candidates in your current CLAUDE.md.

Analyze my CLAUDE.md →

Already know it needs work? The Drills pack ships 25 pre-built Claude Code skills you drop into ~/.claude/skills/. $29 lifetime. Or grab the Tonight Bundle (Drills + Vault) for $39 vs $58 separate.

See Tonight Bundle — $39 →

— The Septim Labs team