All Articles

The Definitive Guide to CLAUDE.md

Arseniy Potapov
Arseniy Potapov
··16 min read·
The Definitive Guide to CLAUDE.md

CLAUDE.md is just one layer. Learn the 5-layer configuration system for Claude Code - with real production examples, common mistakes, and troubleshooting.

I've written tens of CLAUDE.md files across different projects. The shortest is 12 lines. The longest is hundreds of lines covering a multi-tenant SaaS backend with async workers, schema-per-tenant isolation, and a Vue frontend in a separate repo. Researching this article made me realize half my rules belong somewhere else.

Most developers treat CLAUDE.md like a README for AI: dump in your tech stack, add some coding preferences, maybe a few commands. It works well enough until you hit a long session and Claude starts ignoring rules, or you realize you've been writing "NEVER do X" in a file that Claude treats as a suggestion.

CLAUDE.md is one layer of a five-layer configuration system. Settings, hooks, memory, skills - each handles a different type of rule. Put the right rule in the wrong layer and it either gets ignored or creates friction. Put it in the right layer and it just works, every session, without you having to repeat yourself.

By the end of this article, you'll know what goes in each layer, how to structure your CLAUDE.md for maximum impact, and what to move out of it entirely. I'll show real examples from production projects.

If you haven't read my previous article on using Claude Code without creating technical debt, start there for the basics. This one goes deeper.

The Configuration Hierarchy

CLAUDE.md is one file in a five-layer configuration system. Most guides skip this context, which is why developers put enforcement rules in CLAUDE.md and wonder why Claude ignores them on long sessions.

Here's the full stack, from most enforced to most flexible:

block-beta
  columns 1
  A["⚙️ settings.json — Hard denials, permissions"]
  B["🪝 Hooks — Enforce rules at lifecycle events"]
  C["🧠 CLAUDE.md — Context, conventions"]
  D["📓 MEMORY.md — Auto-learned patterns"]
  E["🤲 Skills & Agents — Multi-step workflows"]

  style A fill:#2d2d2d,color:#fff
  style B fill:#444,color:#fff
  style C fill:#666,color:#fff
  style D fill:#888,color:#fff
  style E fill:#aaa,color:#fff

settings.json is the belt. This is where you set hard permissions: which tools Claude can use, which file paths it can read, which commands are blocked. I block bare python3 in my blog project's settings to force venv usage. CLAUDE.md says "use venv python." Settings makes it impossible not to. Three tiers exist: global (~/.claude/settings.json), project (.claude/settings.json, optionally committed to git), and project local (.claude/settings.local.json, gitignored).

CLAUDE.md is the brain. It provides context: your tech stack, coding conventions, project structure, common pitfalls, development commands. Claude reads it at session start, usually follows it, but can forget or override it under context pressure. It's a suggestion, not a guarantee.

MEMORY.md is the notebook. Claude writes this itself. After sessions, it stores patterns it learned: "this project requires tenant_id in every database query" or "user prefers no comments in code." You can edit it, but mostly you let it accumulate. It captures what CLAUDE.md can't anticipate.

Hooks are the muscles. They fire at lifecycle events - before a tool runs, after a tool runs, before context compaction. A hook always executes. Trail of Bits puts it well: "An instruction in your CLAUDE.md saying 'always use pnpm' can be forgotten. A PreToolUse hook that blocks npm install and suggests pnpm fires every single time." Four types exist: shell commands, HTTP endpoints, LLM-as-judge prompts, and full sub-agents that can inspect code before deciding.

Skills and agents are the hands. Reusable multi-step workflows triggered by name. A /review-pr skill runs the same review checklist every time. A documentation search agent queries three different tools (file glob, grep, semantic search) before answering architecture questions. These extend what Claude can do, not what it knows.

Tell in CLAUDE.md, enforce in settings and hooks. If you catch yourself writing "NEVER do X" in CLAUDE.md, that rule should probably be a hook instead. CLAUDE.md is for context and conventions. Settings and hooks are for things Claude must not violate.

When you're unsure which layer to reach for:

  • Claude keeps forgetting a convention - put it in CLAUDE.md and make the instruction more specific
  • Claude must never do something, period - use a PreToolUse hook that returns a deny decision, or a settings.json deny rule
  • Claude should learn something over time - let MEMORY.md handle it, or tell Claude to remember
  • Claude needs to run multiple steps in order - write a skill or an agent definition
  • Different projects need different permissions - use project-level settings.json

Now let's structure the most important layer: CLAUDE.md itself.

Anatomy of a Good CLAUDE.md

The short CLAUDE.md files work fine for simple projects. The long one I mentioned - the multi-tenant SaaS backend - needs every line. Length isn't the issue. Priority order is.

Most CLAUDE.md files I've read start with project overview and tech stack. That's backwards. The highest-value section is the one most people put last or skip entirely: what Claude keeps getting wrong.

Start with Common Pitfalls

This section pays for itself immediately. Every time Claude makes a mistake you've already seen, you add it here. After a month, you have a personalized error-prevention system.

From a SaaS backend project:

## Common Pitfalls
- Never use `db.session` directly in routers - use async sessions via dependency injection
- Never mock the database in tests - use factory-based fixtures with real DB transactions
- Always include `tenant_id` in queries - missing tenant isolation is a security bug
- Don't use `sync_to_async` in API routes - use native async SQLAlchemy sessions
- Read the testing standards guide before writing ANY test

Five lines. Each one prevents a mistake that would take 10-15 minutes to catch in review. Claude reads these at session start and checks against them while coding. This is the highest ROI per line of any CLAUDE.md section.

Then: Tech Stack and Patterns

After pitfalls, give Claude the architectural context it can't infer from code alone. Name your frameworks, your database, your key patterns. Be specific about the things that are invisible in individual files but critical across the codebase.

## Tech Stack
- FastAPI with async SQLAlchemy 2.0 (async sessions in routes, sync in workers)
- Celery with Redis broker (all tasks use `shared_task`, never `app.task`)
- PostgreSQL with schema-per-tenant isolation
- Factory Boy for test fixtures, pytest-asyncio for async tests

The goal isn't listing everything in requirements.txt. It's telling Claude the patterns that deviate from defaults. "Async sessions in routes, sync in workers" is the kind of split that Claude will get wrong without explicit instruction.

Then: Development Commands

Give Claude the exact commands for testing, linting, and building. Not the theory - the actual invocations with your project-specific flags.

## Commands
- Tests: `pytest tests/ -x -q` (stop on first failure)
- Lint: `ruff check . --fix && ruff format .`
- Type check: `mypy src/ --ignore-missing-imports`
- Migrations: `alembic upgrade head` (never edit migration files directly)

Without this section, Claude guesses. You'll watch it run python -m pytest without your flags, or invoke black on a project that uses ruff format. Spell out the commands once and Claude uses them every session.

Documentation Pointers

If your project has architecture docs or testing standards, reference them in CLAUDE.md with explicit priority markers: "CRITICAL: Read docs/api-design-patterns.md before implementing new endpoints." Claude treats capitalized emphasis as high-priority and will actually read the doc before coding. I'll cover documentation strategy in depth in a future article - for now, even two lines of doc pointers save hours of re-explaining patterns in chat.

What Doesn't Belong

Not everything should live in CLAUDE.md. Three categories of content actively hurt when placed here.

Linter config that the linter already enforces. If it's in pyproject.toml, don't restate it in CLAUDE.md. One line pointing to the linter command is enough.

Things Claude already knows. "Write clean code" and "follow best practices" add zero information. Claude defaults to reasonable code. Your CLAUDE.md should override defaults, not restate them.

Philosophy essays. I've seen CLAUDE.md files with 200 words of coding philosophy before any actionable instruction. Claude doesn't need to be inspired. It needs specific rules it can check against while writing code. Save the manifesto for your team wiki.

The test for any CLAUDE.md line: if you removed it, would Claude's output actually get worse? If the answer is "probably not," delete it. Every line costs tokens on every API call. Make each one earn its place.

But CLAUDE.md isn't just one file. Understanding how multiple files stack is where most setups go wrong.

The Layering System in Practice

Claude Code loads CLAUDE.md files from multiple directories and combines them. Understanding how they compose is the difference between a setup that works and one where rules contradict each other.

Four Sections of CLAUDE.md

Your global ~/.claude/CLAUDE.md is your personal coding philosophy. Mine is 18 lines: no comments in code, no inline imports, no try-except by default, short commit messages. These apply to every project I touch, regardless of tech stack.

Your project CLAUDE.md at the repo root can be committed to git. This is where project architecture, commands, and pitfalls live. On small teams, committing it means a new teammate gets all the context automatically. On larger teams where people experiment heavily, keep it short and stable - tech stack, key commands, shared pitfalls - and push personal experiments to the gitignored .claude/CLAUDE.md instead.

Your user project file at .claude/CLAUDE.md (gitignored) holds personal overrides that shouldn't be shared. Maybe you prefer a different test runner flag, or you want Claude to always explain its reasoning. This file stays local.

Directory-level CLAUDE.md files in subdirectories scope rules to specific parts of the codebase. A tests/CLAUDE.md can say "all test classes must inherit from BaseTestCase." An api/CLAUDE.md can specify "all endpoints must include tenant context middleware."

These compose cleanly. My global "no try-except" rule works alongside my project-specific "use async sessions in routers, sync sessions in Celery workers." Claude sees both and follows both. When rules conflict, more specific files (deeper directories) take priority.

How Other Tools Handle This

Half my team uses Cursor alongside Claude Code, so I had to figure out whether rules transfer between tools. The short answer: the concepts are identical, the file formats differ.

Cursor uses .cursor/rules/*.mdc with YAML frontmatter and glob scoping. You can write one rule file for *.py and another for *.tsx. That's genuinely useful for polyglot projects and something Claude Code lacks. Codex uses AGENTS.md with the same directory hierarchy as CLAUDE.md, plus an exec policy engine for enforcement. Copilot uses .github/copilot-instructions.md. Windsurf uses .windsurf/rules/*.md. Gemini reads AGENTS.md as a fallback alongside its own GEMINI.md.

I prefer Claude Code's plain markdown approach. No frontmatter to remember, no special syntax, nothing to learn. The tradeoff is no per-file-type scoping, which matters in monorepos but not in most projects I work on.

If you're on a team that mixes tools, keep your CLAUDE.md rules general enough that they'd work in a .cursorrules file too. The rules you'd write for one tool translate directly to the others. The concept is universal: a markdown file in your repo that tells the AI how to behave.

When CLAUDE.md Isn't Enough

You know which layer handles what. Now let's look at the mechanics of promoting rules out of CLAUDE.md when they need enforcement.

Promote Rules to Hooks

Every time you catch Claude violating a CLAUDE.md rule for the third time, consider promoting it to a hook. A PreToolUse hook runs before Claude executes any tool. It can inspect the command, check for violations, and return a JSON decision to deny it.

I have a hook that enforces pnpm in projects where someone might run npm install out of habit. CLAUDE.md says "use pnpm." The hook guarantees it. Another useful one: blocking edits to migration files, because Claude loves to "fix" them instead of creating a new migration. A PostToolUse hook can auto-format code after every file edit so you never have to remind Claude to run the linter.

Here's the package manager hook in .claude/settings.local.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/enforce-pnpm.sh"
          }
        ]
      }
    ]
  }
}

The script itself is short:

#!/bin/bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$CMD" | grep -qE '^\s*npm\s+(install|i|ci|add|remove)\b'; then
  echo '{"hookSpecificOutput": {"permissionDecision": "deny",
    "permissionDecisionReason": "use pnpm, not npm"}}'
fi

It reads JSON from stdin, checks if the command is an npm install variant, and returns a deny decision. Exit code 0 with no output means allow. Beyond shell scripts, hooks come in four types: command (shell), http (POST to an endpoint), prompt (LLM-as-judge for yes/no decisions), and agent (a sub-agent that can read files and inspect code before deciding). The hooks reference covers all lifecycle events and their input schemas. The threshold for when to write one is simple: if it's a convention Claude sometimes forgets, keep it in CLAUDE.md. If a single violation wastes your time or breaks the build, make it a hook.

Use Settings for Hard Denials

settings.json deny rules block operations before Claude even tries them. My blog project denies bare python3 commands, forcing Claude to use .venv/bin/python. The SaaS project denies reading ~/.ssh/**, ~/.aws/**, and ~/.gnupg/** because no development task should touch credentials.

Deny rules are absolute. No context pressure, no forgotten instructions, no "but I thought you meant..." excuses. Use them for credential protection and destructive command blocking:

{
  "deny": [
    "Bash(python3:*)",
    "Read(~/.ssh/**)",
    "Read(~/.aws/**)",
    "Bash(rm -rf:*)",
    "Bash(git push --force:*)"
  ]
}

Reach for Skills and Agents

When you catch yourself typing the same multi-step instruction for the third time - "search the docs directory, then check the API reference, then look at the test fixtures" - that's a skill or an agent.

A skill is a markdown file in .claude/skills/ that Claude can invoke with a slash command. /review-pr always runs the same checklist. /generate-audio always follows the same pipeline. The instructions live in a versioned file, not in your head.

An agent is a more complex definition with its own system prompt, model choice, and tool restrictions. My documentation search agent uses three tools in sequence (file glob for filenames, grep for content, semantic search via Qdrant for concepts) and returns structured results with relevance scores. It runs on haiku to keep costs down. This is overkill for most projects - I only built it after the codebase reached 40K lines and grep stopped finding what I needed reliably.

The common thread: if you're explaining a workflow to Claude in chat, it belongs in a file. CLAUDE.md for context, skills for procedures, agents for specialized capabilities.

Why Instructions Get Ignored

Even with rules in the right layer, instructions still fail. You write a clear rule in CLAUDE.md. Claude follows it for 20 minutes, then violates it mid-session. This isn't random. There are specific mechanisms that cause instruction loss, and once you know them, you can work around each one.

Context Compaction

Claude Code has a finite context window. When the conversation gets long, it compresses earlier messages to make room. Your CLAUDE.md content gets loaded at session start, but after compaction, it's summarized with a disclaimer: "this context may or may not be relevant to your tasks." That framing weakens every instruction in the file. Rules that were absolute become suggestions that Claude weighs against its current task.

The fix is structural, not verbal. Short, specific rules survive compaction better than paragraphs of explanation. "Never use db.session directly in routers" compresses to the same rule. "We prefer to use dependency-injected async sessions because of our multi-tenant architecture where each tenant has its own schema and we need to ensure proper isolation through the session factory" compresses to something vague about sessions and tenants. If you find yourself re-explaining a rule in chat, it should be shorter and more direct in the file.

There's a PreCompact hook that fires before compaction happens. It can't prevent compaction or control what gets kept, but it can run a script. I built a pattern around this that I haven't seen documented anywhere: use PreCompact to snapshot your working state into MEMORY.md before compaction destroys it.

The hook receives the full conversation transcript path via stdin JSON. A Python script reads it as JSONL, extracts what you're working on (current task, key file paths, recent decisions), and does two things. First, it writes a recovery state file with everything the next context needs to pick up where you left off. Second - and this is the critical part - it injects a timestamped alert block at the top of MEMORY.md:

## !! POST-COMPACTION RECOVERY !!

**Compacted at:** 2026-03-01 01:13
**Active article:** `claude-md-guide`

**STOP. Before responding to the user, do these in order:**
1. Read `/path/to/state-file.md` for full state
2. Read the relevant task files listed there
3. Only THEN respond to the user

Why MEMORY.md? Because Claude re-reads it after compaction. Your CLAUDE.md instructions say "read the state file after compaction" - but after compaction, those instructions arrive wrapped in "may or may not be relevant." A dynamic alert injected into the first lines of MEMORY.md hits harder than a static rule in CLAUDE.md that the compaction summary has already downgraded.

The hook config in .claude/settings.local.json:

{
  "hooks": {
    "PreCompact": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".venv/bin/python scripts/precompact_snapshot.py"
          }
        ]
      }
    ]
  }
}

I learned two things building this. First, the script must be idempotent - if compaction fires twice, you get two alert blocks in MEMORY.md unless you strip the previous one before injecting. Second, static CLAUDE.md instructions alone weren't enough. I had "read the state file after compaction" in CLAUDE.md for weeks. Claude ignored it every time. The dynamic injection into MEMORY.md was the fix that actually worked, because it puts the instruction where Claude is already looking, not where compaction has weakened its authority.

Contradictory Rules

When your global ~/.claude/CLAUDE.md says "no try-except" and your project CLAUDE.md says "wrap all API calls in error handling," Claude resolves the conflict by sometimes doing one, sometimes the other. Project-level rules usually win (more specific context), but not reliably.

Audit across layers. Read your global, project, and directory-level files back to back. When two rules conflict, make one explicitly override the other: "Exception to the global no-try-except rule: third-party API calls in this project must use retry logic with exponential backoff."

Token Bloat

A 500-line CLAUDE.md doesn't just cost tokens - it pushes Claude toward skimming. I've seen Claude follow a rule at the top of a 400-line file and miss one at the bottom.

The fix: move enforcement rules to hooks or settings deny rules, where they can't be compacted away. The hierarchy section above covers which layer to use.

Common Mistakes

These mistakes show up constantly. I've caught most of them in my own files before figuring out the pattern.

Writing a novel. Some projects genuinely need a long CLAUDE.md. But I've seen files over 500 lines that repeat the same point three different ways, include entire API references, or paste in full configuration files. Claude doesn't read more carefully when you write more. Past a certain point, it skims. If your CLAUDE.md is longer than your actual README, something belongs in a separate doc that Claude reads on demand.

Vague rules. "Ensure proper error handling" gives Claude no way to check its output against the instruction. A rule that works: "never wrap code in try-except unless you're in a loop that must continue on failure, or retrying a third-party API call." That's specific enough to be unambiguous. Every rule should pass this test: can Claude determine from the instruction alone whether its output follows it?

Duplicating the linter. I've seen CLAUDE.md files listing individual ruff rules, max line lengths, and import ordering preferences. All of that already lives in pyproject.toml or .ruff.toml. Claude reads those files when it runs the linter. Your CLAUDE.md should say "run ruff check . --fix before committing" and nothing more. Duplicating linter config creates version drift - you update pyproject.toml and forget to update CLAUDE.md, and now Claude has contradictory instructions.

Not evolving the file. I add 2-3 lines per week to my most active project's CLAUDE.md, almost always after catching a repeated mistake. If you wrote yours in one sitting and haven't touched it since, you're leaving the most valuable rules undocumented.

Ignoring auto-memory. MEMORY.md exists specifically to capture things you didn't anticipate. If you've never checked what Claude has written in ~/.claude/projects/.../MEMORY.md, you're missing useful context that accumulated automatically. Review it occasionally. When a MEMORY.md entry proves consistently useful, promote it to CLAUDE.md where it's persistent and visible at session start.

Start With Five Lines

CLAUDE.md is the brain of your Claude Code setup, but a brain without muscles, memory, and hands doesn't get much done.

If you take one thing from this article: add a Common Pitfalls section to your CLAUDE.md today. Open your last 3 Claude Code sessions, find the mistakes you corrected, and write them as specific rules. Five lines of "don't do X, do Y instead" will save you more time than 50 lines of project philosophy.

Then audit the rest. Any rule that says "NEVER" probably belongs in a hook or a settings deny rule. Any pattern Claude keeps rediscovering belongs in MEMORY.md. Any multi-step workflow you keep describing in chat belongs in a skill.

I'm still iterating on my own setup. My longest CLAUDE.md grew by 45 lines while writing this article, mostly from realizing I'd been correcting the same mistakes in chat instead of documenting them. Your CLAUDE.md should grow from the mistakes you catch, not from a planning session.

I'll cover hooks patterns and documentation strategy (writing docs that AI can actually use) in separate articles.

© 2026, built by Arseniy Potapov with Gatsby