CLAUDE.md: the file every AI-coding team keeps rewriting

Justin Adams ·
claude-code ai-coding team-workflow best-practices

Every team using Claude Code eventually writes a CLAUDE.md. Most write it three times before it’s right. The first version is five lines and Claude ignores half of them. The second is 200 lines and Claude gets confused by the contradictions. The third — if the team gets there — is specific, structured, and actually changes how Claude behaves. This post shows you what the third version looks like.

What CLAUDE.md is and why it matters

CLAUDE.md is a markdown file placed at the root of your repo (or in ~/.claude/ for user-level configuration) that Claude Code reads at the start of every session. It functions as persistent context injection — everything you’d otherwise repeat in a prompt, you write once here.

Claude looks for it in a specific order: the project root first, then .claude/CLAUDE.md, then the user-level ~/.claude/CLAUDE.md. Project-level config wins for team standards; user-level is for personal preferences. This post focuses on project-level.

Why does this matter for teams? Claude’s behavior is shaped by its context. If every engineer on your team opens a session with different context — different instructions, different assumptions, different constraints — Claude will behave differently for each of them. CLAUDE.md is how you make that behavior consistent. It’s team configuration, not a personal dotfile.

The anatomy of a good CLAUDE.md

Here’s a full, working example with annotations explaining what each section does and what breaks when you skip it.

# Project: Acme API

A multi-tenant REST API built with Hono on Bun. PostgreSQL via Supabase,
Drizzle ORM for queries, JWT auth. TypeScript strict throughout.

# ANNOTATION: The project context block tells Claude what it's working in

# before it reads a single line of code. Without it, Claude makes assumptions

# about your stack and gets the idioms wrong from line one.

## Stack

- Runtime: Bun
- Framework: Hono + @hono/zod-openapi
- Database: PostgreSQL (Supabase), Drizzle ORM
- Auth: Supabase Auth + custom JWT validation
- Tests: bun:test (no Jest, no Vitest)

## Code style

- Use Zod for all input validation — never Yup, never manual checks
- Throw typed errors using our AppError class, never raw Error()
- All API responses follow { data, error } envelope — no naked returns
- Prefer explicit over clever; name things for the next reader

# ANNOTATION: Without explicit style rules, Claude will write valid code that

# doesn't match your codebase. Zod vs. Yup, error envelope vs. raw throws —

# these feel minor until a PR review has to catch them every time.

## Testing

- Write a failing test before any implementation (red/green TDD)
- Test command: bun test
- Do not mock the database in unit tests — use test fixtures instead
- Integration tests live in src/**tests**/<route>.test.ts (flat, not nested)

# ANNOTATION: "Write tests" is too vague. Claude needs to know the test

# runner, the file convention, and the mocking philosophy — otherwise it

# will reach for Jest patterns in a bun:test repo and silently break CI.

## Protected paths

- packages/db/drizzle/\*\* — auto-generated by drizzle-kit; NEVER edit manually
- apps/web/src/routeTree.gen.ts — auto-generated by TanStack Router
- packages/auth/src/ — auth bugs are security incidents; flag before touching

# ANNOTATION: Without this section, Claude will helpfully "fix" an auto-

# generated file and corrupt it. One sentence per path is enough.

## Common commands

bun run build # build all workspaces
bun run dev # start dev server
bun test # run test suite
bun run typecheck # tsc --noEmit across all packages

# ANNOTATION: Claude will guess at commands if you don't list them.

# It will guess wrong — often close enough to waste time debugging.

## Commit style

Use Conventional Commits: feat, fix, docs, refactor, test, chore.
Scope to the affected workspace: feat(api): add rate limiting endpoint
Body required when the why isn't obvious from the title.

## What "done" means

A task is done when:

1. bun test passes with no new failures
2. bun run typecheck exits clean
3. Protected paths are untouched (unless explicitly instructed)
4. The change is scoped to the stated task — no bonus refactors

Every section is load-bearing. The “what done means” section is the one most teams skip and regret most — without it, Claude will mark work complete based on its own judgment of completeness, which is rarely identical to yours.

Five CLAUDE.md patterns worth stealing

1. The “never touch these files” guard

List your auto-generated files explicitly. Claude has no way to know that routeTree.gen.ts is owned by a code generator, not a human, unless you tell it. One accidental edit and you’re debugging a corrupted file tree instead of shipping.

## Protected paths

- apps/web/src/routeTree.gen.ts — generated by TanStack Router (bun run dev regenerates)
- packages/db/drizzle/\*\* — generated by drizzle-kit; run db:generate to update
- coverage/ — generated by test runner; never commit

Never modify these files directly. If a change requires updating generated output,
run the generator and commit the result.

2. The “ask before doing X” brake

Some operations are expensive to reverse: database migrations, file deletions, force pushes. This pattern tells Claude to pause and explain before executing — not as a philosophical principle, but as a literal instruction it will follow.

## High-risk operations — confirm before running

Before running any database migration: describe the change, list affected tables,
and wait for confirmation. Do not run drizzle-kit generate or migrate autonomously.

Before deleting any file: list the file path and state why it's safe to remove.

Before any git operation that rewrites history (rebase, force push, reset --hard):
stop and surface the situation. Do not proceed.

3. The preferred patterns library

This is the most underrated pattern on this list. Instead of describing your codebase’s conventions in prose, show Claude a reference example for each recurring construct — how you throw errors, how you type API responses, how you structure a new route. Claude will match it precisely.

## Preferred patterns

### API route (Hono + zod-openapi)

```ts
app.openapi(
  createRoute({
    method: "get",
    path: "/items/{id}",
    request: { params: ParamSchema },
    responses: {
      200: { content: { "application/json": { schema: ItemSchema } } },
    },
  }),
  async (c) => {
    const { id } = c.req.valid("param");
    const item = await db.query.items.findFirst({ where: eq(items.id, id) });
    if (!item) throw new AppError("NOT_FOUND", 404);
    return c.json({ data: item });
  },
);
```

Error handling

Throw AppError, never raw Error. AppError(code, statusCode) — codes are uppercase snake_case strings (‘NOT_FOUND’, ‘UNAUTHORIZED’, ‘VALIDATION_ERROR’).


### 4. The test-first enforcement

TDD is easy to intend and easy to skip. This pattern makes it structural — Claude writes the failing test first because the instruction is explicit, not because it's in the right mood. Include the exact command so Claude can verify red before writing implementation.

```markdown
## Test-first development

For every bug fix and feature implementation:
1. Write a failing test that captures the expected behavior (bun test to verify it fails)
2. Implement the minimum code to make it pass
3. Confirm bun test exits clean

Skip this only for: config changes, documentation, and database migrations.
If a failing test already exists that covers the behavior, note it and proceed
to the implementation step.

5. The scope discipline guardrail

Large codebases make scope creep easy. Claude sees a related issue, fixes it, and your targeted 2-file change becomes a 12-file refactor with unreviewed surface area. This pattern forces a scope declaration before Claude touches anything.

## Scope discipline

Before making any change that touches more than 3 files or crosses package
boundaries, stop and list:

- Every file you intend to modify
- Why each file is necessary for this specific task

Wait for confirmation before proceeding. If the task grows mid-implementation,
pause and re-confirm scope rather than continuing.

Editorial changes (copy, comments) must not cascade into renames or refactors.

How to version and share CLAUDE.md across your team

Commit CLAUDE.md to the repo. It’s team configuration — the same way you’d commit .eslintrc or a Dockerfile. Every developer gets it on git clone, every Claude Code session respects the same rules, and changes go through code review.

For user-level config (~/.claude/CLAUDE.md): this is personal. Don’t try to sync it across the team via git. Use it for individual preferences — your preferred verbosity level, personal shortcuts, how you like commit messages formatted. Team standards belong in the project file.

The practical version-control recommendation: treat changes to CLAUDE.md the same way you treat changes to CI configuration. PR required, at least one reviewer. A bad change to your CI config fails one build; a bad change to CLAUDE.md changes how Claude behaves for everyone, quietly, until someone notices the drift.

For teams managing CLAUDE.md patterns across 20+ seats, the project-level file has limits — it doesn’t handle org-scoped distribution or per-team variations. That’s where Cendis platform distribution picks up.

If you want a starting point rather than a blank file, the Cendis Skill Library has CLAUDE.md templates for React/TypeScript, Python, and Go codebases — ready to download and adapt.


The third version of your CLAUDE.md isn’t longer than the first two — it’s more specific. It replaces vague intentions with concrete instructions, lists the files Claude must never touch, shows Claude what your code looks like instead of describing it, and defines done in terms Claude can actually verify. If you don’t want to build that from scratch, the Cendis Skill Library has templates for the most common stacks. Browse and install in under two minutes.