Paths Subjects Questions Quizzes Pricing Search

CLAUDE.md and Context Configuration

Configuring durable memory, permissions, and guardrails for Claude Code

CLAUDE.md and Context Configuration

Every Claude Code session starts from a blank context window. The agent that helped you refactor your billing module yesterday remembers nothing about it today — no build commands, no "we tried that and it broke prod," no "this directory looks unused but three cron jobs depend on it." Unlike a human teammate, it cannot accumulate tacit knowledge just by sticking around.

CLAUDE.md, auto memory, and settings.json are the mechanisms that close this gap. They let you (and, increasingly, Claude itself) write down what would otherwise have to be re-explained every session, and they let you back the agent's judgment with rules that don't depend on judgment at all. This subject covers how to configure all three well — and, just as importantly, how badly-configured versions of each fail in practice.


Why CLAUDE.md Exists

Two problems motivate CLAUDE.md:

  1. Agents lose context between sessions. A fresh Claude Code session has no memory of prior conversations. If your project has an unusual test setup, a deploy command that isn't npm run deploy, or a "never touch this file" rule, the agent has to rediscover — or be told — all of it again.
  2. Humans shouldn't have to re-explain conventions every time. If you find yourself typing the same correction into chat every few sessions ("use pnpm, not npm," "API handlers go in src/api/handlers/"), that correction belongs in a file the agent reads automatically, not in your fingers.

CLAUDE.md is a plain markdown file that Claude Code reads at the start of every session, before you type anything. It sits alongside — but is distinct from — the system prompt: CLAUDE.md content is delivered as a user-role message after the system prompt, which is part of why it shapes behavior rather than strictly enforcing it (more on that distinction in the hooks section).

Session 1 (Monday):                    Session 2 (Wednesday):
┌─────────────────────────┐            ┌─────────────────────────┐
 Fresh context window                  Fresh context window     
 + CLAUDE.md loaded                    + CLAUDE.md loaded       
 + You explain the repo     ────X──▶   + You explain the repo   
   ("tests need Redis")     (lost)       AGAIN, unless it's     │
                                         written down           
└─────────────────────────┘            └─────────────────────────┘

Claude Code actually ships two complementary memory systems, and it's worth distinguishing them before going further:

CLAUDE.md files Auto memory
Who writes it You Claude
What it contains Instructions and rules Learnings and patterns Claude discovered
Loaded into Every session, in full Every session (first 200 lines or 25KB of MEMORY.md)
Best for Coding standards, workflows, architecture Debugging insights, discovered quirks, preferences you stated in chat
Enforcement Advisory context — Claude tries to follow it Advisory context — same as CLAUDE.md

Auto memory (on by default) lets Claude write itself notes in ~/.claude/projects/<project>/memory/ — an index file MEMORY.md plus topic files it creates as needed — when you correct it or when it notices a durable pattern. It's genuinely useful, but it's not a substitute for CLAUDE.md: auto memory is discovered, unreviewed by default, and machine-local (not shared with your team via git). CLAUDE.md is authored, reviewed like any other file in version control, and shared with everyone who clones the repo. Most of this subject focuses on CLAUDE.md and the configuration layers around it, since that's what your team collectively owns — but auto memory gets a full treatment of its own later on, since it's a distinct system with its own mechanics worth understanding precisely, not just a footnote to CLAUDE.md.


The Memory Hierarchy

CLAUDE.md files can live at four scopes, loaded in this order (broadest to most specific — so more specific instructions are read last, closer to your actual prompt):

Scope Location Purpose Shared with
Managed policy /etc/claude-code/CLAUDE.md (Linux), /Library/Application Support/ClaudeCode/CLAUDE.md (macOS), or via claudeMd in managed settings Org-wide rules (compliance, security policy) Everyone on the machine, every repo
User ~/.claude/CLAUDE.md Your personal preferences across all projects Just you
Project ./CLAUDE.md or ./.claude/CLAUDE.md Team-shared project instructions Team, via version control
Local ./CLAUDE.local.md Your personal per-project notes (sandbox URLs, local test data) Just you, this project — gitignore it

A critical detail: these files are concatenated, not overridden. If your user-level CLAUDE.md says "always write commit messages in imperative mood" and the project's CLAUDE.md doesn't mention it, both apply — there's no single file that wins. Within a project, Claude Code also walks the directory tree from the filesystem root down to your working directory, loading any CLAUDE.md / CLAUDE.local.md it finds along the way. A CLAUDE.md in a subdirectory below your working directory isn't loaded at launch — it's picked up on demand, the moment Claude reads a file that lives in that subdirectory.

~/repo/                     foo/CLAUDE.md loaded at launch, first
├── CLAUDE.md
├── CLAUDE.local.md         appended right after CLAUDE.md at this level
└── services/
    └── billing/
        └── CLAUDE.md       NOT loaded at launch; loaded only when
                              Claude reads a file under services/billing/

This has a practical consequence: a monorepo can push detail down into the subdirectories that need it (a services/billing/CLAUDE.md covering billing-specific quirks) instead of forcing every session — even ones that never touch billing — to pay for that context. For instructions scoped to a file type rather than a directory (e.g., "all TypeScript files under src/api/ need input validation"), Claude Code offers .claude/rules/*.md with paths: frontmatter — a more structured version of the same idea, matched by glob against the files Claude is actually working with.

Precedence conflict: if two loaded files genuinely disagree (one says "use tabs," another says "use spaces"), Claude may pick one arbitrarily — there's no tie-breaking rule beyond load order. This is worth remembering: periodically audit your CLAUDE.md files for contradictions rather than assuming layering resolves them for you.


Auto Memory: A Persistent System of Its Own

The table earlier in this subject introduced auto memory as CLAUDE.md's sibling — but it's easy to undersell just how different the two mechanisms are mechanically. CLAUDE.md is a file (or several) you write, commit, and maintain like any other project artifact. Auto memory is a whole subsystem: a directory Claude reads and writes to on its own, with its own index file, its own size limits, its own on-demand loading rules, and its own lifecycle separate from CLAUDE.md's "load everything, every time" model. It's worth treating as a first-class mechanism, not a footnote.

Where it lives. Each project gets its own memory directory at ~/.claude/projects/<project>/memory/. The <project> path is derived from the git repository, so every worktree and every subdirectory of the same repo shares one memory directory — memory isn't per-checkout, it's per-repo. Outside a git repository, the project root is used instead. This location is machine-local by default: it's not inside your repo, so it's never committed and never shared with teammates by cloning. (You can relocate it with autoMemoryDirectory in settings if you want, for example, to point it at a synced location — but the default is intentionally local.)

What's inside. The directory contains one entrypoint plus however many topic files Claude has decided to create:

~/.claude/projects/<project>/memory/
├── MEMORY.md          # concise index, loaded into every session
├── debugging.md       # detailed notes on a topic Claude split out
├── api-conventions.md # another topic file
└── ...                # created as needed — not a fixed set

MEMORY.md is an index, not a dumping ground. Claude is expected to keep entries to roughly one line each, with detail pushed into topic files and linked from the index:

# Memory Index

## Project
- [build-and-test.md](build-and-test.md): npm run build (~45s), Vitest, dev server on 3001
- [architecture.md](architecture.md): API client singleton, refresh-token auth

## Reference
- [debugging.md](debugging.md): auth token rotation and DB connection troubleshooting

This one-fact-per-file discipline is enforced, not just suggested. The first 200 lines of MEMORY.md, or its first 25KB (whichever limit comes first), are loaded at the start of every session — content past that point simply isn't read. When Claude writes to MEMORY.md, Claude Code checks the file against those limits: if it's getting close, Claude is reminded to shorten it (one line per entry, move detail out, drop stale entries); if it's already over the limit, the write still succeeds but Claude Code returns an error telling Claude to rewrite the index, because everything past the cutoff would otherwise be silently invisible on the next load. This cap applies only to MEMORY.md — the topic files it links to have no size limit of their own, because they're not loaded at startup at all. Claude reads a topic file on demand, with its normal file tools, only when the current task relates to it. Compare this to CLAUDE.md, which is loaded in full regardless of length (shorter still produces better adherence, but there's no hard cutoff the way there is for the memory index).

Who writes it, and when. Auto memory is on by default, and Claude decides what's worth saving — it doesn't write something every session. The trigger is usually one of two things:

  1. You correct Claude or state a preference in chat. If you say something like "always use pnpm, not npm" or "remember that the API tests need a local Redis instance running first," Claude saves that to auto memory on its own — you don't have to ask it to. This is the everyday case: auto memory exists precisely so these one-off corrections don't evaporate at the end of the session the way an unrecorded chat message would.
  2. Claude notices a durable pattern while working — a build quirk, a debugging insight, an architectural detail it had to work out the hard way — and judges it worth recording for a future session, without you prompting it at all.

You'll see this happening in the interface as messages like "Saved 2 memories" or "Recalled 2 memories," which correspond to real reads and writes against the memory directory.

Auto memory vs. asking for CLAUDE.md. This is the everyday decision point: if you tell Claude to remember something in chat, it defaults to auto memory, not CLAUDE.md — the two are not interchangeable destinations for "things Claude should know." If you specifically want an instruction to land in CLAUDE.md instead (because it's a team-shared rule, not a personal or discovered note), you have to say so explicitly — "add this to CLAUDE.md" — or edit the file yourself via /memory. Left to its own judgment, Claude routes discovered facts and casual corrections to auto memory, which is the right default: most of what comes up in a session is exactly the kind of low-stakes, personal, or provisional note auto memory is for, not a team policy that belongs in version control.

Enabling, disabling, and relocating it. Auto memory is on by default. Toggle it from within a session with /memory (which saves autoMemoryEnabled to your user-level ~/.claude/settings.json), or turn it off for a single project by setting "autoMemoryEnabled": false in that project's settings.json. An environment variable, CLAUDE_CODE_DISABLE_AUTO_MEMORY=1, disables it without touching any settings file at all — useful for CI or headless runs where you don't want Claude accumulating notes about a throwaway checkout.

Retention. Session transcripts age out and get deleted after cleanupPeriodDays (30 days by default). Auto memory is deliberately excluded from that sweep — MEMORY.md and its topic files persist until you or Claude edits or deletes them, not until a retention timer expires. This is a meaningful asymmetry: your memory notes are treated as durable knowledge, not session exhaust, even though they live outside version control.

Subagents get their own, separate memory. The auto memory described here belongs to your main session. It is not loaded into subagents by default — a subagent starts with a clean slate unless it's a fork of the current conversation (which inherits the parent's context wholesale). A subagent can maintain its own persistent memory if its definition sets a memory field, but that writes to a distinct directory (.claude/agent-memory/<name>/ for project-scoped, .claude/agent-memory-local/ to keep it out of git, or ~/.claude/agent-memory/<name>/ for cross-project) — a different feature from your main session's memory, not a shared pool.

Auditing and editing memory with /memory

/memory is the entry point for working with both halves of the memory story. Running it in a session lists your CLAUDE.md, CLAUDE.local.md, and memory file locations across user and project scopes — including entries for files that don't exist yet, so you can see at a glance where you could add something. From that list you can:

  • Toggle auto memory on or off for your user scope.
  • Open the auto memory folder directly, to browse what Claude has actually saved.
  • Select any file to open it in your editor. Selecting one that doesn't exist yet creates it first. GUI editors (like VS Code) open the file in a separate window without blocking the session — you can keep working while it's open; terminal editors take over until you exit.

Because everything in the memory directory is plain markdown, there's nothing special about editing it by hand — you can open MEMORY.md or any topic file and rewrite, trim, or delete it exactly as you would any other file in your project. If you're not sure whether something actually made it into context for the current session (as opposed to whether it exists on disk), /memory isn't the tool for that — run /context instead, which reports what actually loaded, including which CLAUDE.md and memory files were picked up.


What Belongs in CLAUDE.md (and What Doesn't)

This is the single highest-leverage thing to get right, and the most common way CLAUDE.md files go bad in practice.

The anti-pattern: treating CLAUDE.md as project documentation. Engineers write a file that restates the directory layout, lists every dependency in package.json, and narrates the architecture diagram — content Claude can derive perfectly well by reading the code itself. This bloats the file, burns context budget every single session, and — per Claude Code's own guidance — reduces instruction adherence, because the signal (the one weird rule that actually matters) gets buried in noise the agent has already inferred.

A useful test: would a competent engineer derive this fact by reading the code for five minutes, or would they get it wrong / get bitten by it first? If the former, leave it out. If the latter, it belongs in CLAUDE.md.

Belongs in CLAUDE.md Doesn't belong
Run tests with make test-unit, not pytest directly — it sets FIXTURE_DB. A description of what a unit test is
The /legacy-api directory is dead code kept for one client; don't extend it. A full directory tree listing
We use soft deletes everywhere — never write a hard DELETE against user tables. "This project uses PostgreSQL for its database"
Auth tokens are validated in middleware/auth.py, not per-endpoint. A list of every file in the repo
CI requires conventional commits (feat:, fix:, chore:). "Write clean, readable code"

Notice the pattern: the good entries are things that surprise the agent — deviations from framework defaults, historical decisions, footguns, non-obvious commands. The bad entries are things any agent would get right by reading two files, or are so vague ("write clean code") that they give the agent nothing concrete to act on.

Official guidance frames it the same way: treat CLAUDE.md as the place you write down what you'd otherwise re-explain — add an entry when Claude makes the same mistake twice, when code review catches something Claude should have known, when you type the same correction into chat that you typed last session, or when a new teammate would need the same context to be productive. That's a good litmus test to apply before adding anything.

Specificity beats vagueness. Compare:

Bad:  "Format code properly."
Good: "Use 2-space indentation; run `npm run lint:fix` before committing."

Bad:  "Test your changes."
Good: "Run `npm test` before committing; integration tests require
       `docker compose up -d postgres` first."

Bad:  "Keep files organized."
Good: "API handlers live in src/api/handlers/, one file per resource."

Vague instructions are unverifiable — neither you nor Claude can tell whether they were followed. Concrete instructions are checkable, and checkable instructions get followed more consistently.

Target size: keep a CLAUDE.md file under roughly 200 lines. Past that, it consumes a disproportionate share of every session's starting context and instruction adherence measurably drops. If you're past that and still have more to say, that's a signal to split by directory (nested CLAUDE.md) or by file type (.claude/rules/ with paths: scoping), not to keep appending to one file.


Context Management: Why This Gets More Important as Sessions Grow

Every Claude Code session runs inside a fixed-size context window (on the order of 200K tokens for the default model). Understanding what fills that window — and in what order — explains why a lean CLAUDE.md matters more than it might seem at first glance.

At session start, before you've typed a single message, the window already contains:

┌─────────────────────────────────────────────────────┐
 System prompt              (core behavior, fixed)       ~thousands of tokens
 Auto memory (MEMORY.md)    (first 200 lines / 25KB)   
 Environment info           (cwd, platform, git state) 
 MCP tool listings          (names only; schemas       
                              deferred until needed)    
 CLAUDE.md files             (all scopes, concatenated)    this is what you author
└─────────────────────────────────────────────────────┘
                    
            Your conversation begins here,
            in whatever budget remains

As the conversation proceeds, every tool call, file read, and command output adds tokens. A Read on a 2,000-line file, a verbose test run, a long git log — these accumulate fast, and unlike CLAUDE.md they're one-time costs per session, not a tax paid at every launch. Eventually the window approaches its limit, and Claude Code auto-compacts: it summarizes older parts of the conversation to free up room, keeping the session usable without you manually restarting it.

What survives compaction matters. The project-root CLAUDE.md is re-read from disk and re-injected into context after compaction — it's treated as durable, foundational context worth restoring. Nested CLAUDE.md files (in subdirectories) and path-scoped rules are not automatically re-injected; they reload the next time Claude happens to read a file that triggers them. If an instruction seems to have "vanished" mid-session, this is usually why: it was either said only in chat (and chat is exactly what compaction summarizes away), or it lived in a nested file that hasn't been re-triggered since compaction ran.

Two practical consequences follow:

  1. Content that must survive the whole session belongs in the root CLAUDE.md, not in chat. If you tell Claude something important verbally mid-session ("actually, never touch the payments table directly"), that instruction is vulnerable to being summarized away later. Promoting it to CLAUDE.md makes it durable.
  2. Bloat compounds. A 400-line CLAUDE.md doesn't just cost tokens once — it's re-read in full at the start of every session and re-injected after every compaction, for every engineer on the team, for the life of the project. A 40-line file that only contains genuine surprises pays for itself many times over; a 400-line file that restates the codebase is a recurring tax with no offsetting benefit.

settings.json Basics

Where CLAUDE.md shapes behavior (advisory, probabilistic), settings.json controls what the agent is allowed to do (enforced by the client, not by Claude's judgment). It layers the same way CLAUDE.md does, across four scopes, with the highest-priority scope winning on conflicting individual settings:

Scope Location Shared with
Managed Deployed by IT/MDM, e.g. managed-settings.json All org members — cannot be overridden
Local .claude/settings.local.json Just you, this repo — gitignore it
Project .claude/settings.json Team, via git
User ~/.claude/settings.json Just you, all projects

Important exception: permissions rules don't follow this override-by-priority model — they merge across scopes. A deny rule set anywhere in the hierarchy stays in effect; you can't accidentally un-deny something from a lower-priority scope.

Permissions: scoping trust

The permissions block controls which tool calls run automatically versus which require your approval:

{
  "permissions": {
    "allow": [
      "Bash(npm run lint)",
      "Bash(npm run test *)",
      "Read(~/.zshrc)"
    ],
    "deny": [
      "Bash(curl *)",
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(./secrets/**)"
    ]
  }
}

The pattern matters more than any specific rule: allow entries are commands/paths you trust enough to run without a prompt each time (typically read-only or low-risk — lint, test, formatted diffs). deny entries are hard blocks — things Claude should never be allowed to do regardless of how a task is phrased, like reading .env files or making arbitrary network calls via curl. Everything not explicitly allowed or denied falls back to interactive approval.

Scoping trust well means thinking about blast radius, not convenience. A tempting shortcut is to broadly allow Bash(*) so you stop getting interrupted — but that also silently allows rm -rf, arbitrary git push --force, and anything else the agent might construct as a shell command, because tool-syntax wildcards match on the command prefix, not on intent. Prefer allowing specific, narrow command patterns (Bash(npm run test *)) over broad ones (Bash(npm *) or, worse, Bash(*)), and put genuinely destructive operations in deny explicitly rather than relying on omission from allow.

Settings also carry other everyday fields worth knowing: model (which model to use), env (inject environment variables into Claude Code's runtime), cleanupPeriodDays (session data retention), autoCompactEnabled, and autoMemoryEnabled. These are straightforward key/value toggles — the permissions design is where the real judgment calls live.


Hooks: Deterministic Guardrails Around a Probabilistic Agent

CLAUDE.md and auto memory are both context — Claude reads them, tries to follow them, and usually does, but there's no guarantee. For instructions that must hold every time regardless of what the agent decides — "never let a commit touch prod.env," "always run the linter after an edit," "block any rm -rf" — context isn't strong enough. That's what hooks are for.

A hook is a shell command (or HTTP endpoint, or MCP tool call) that Claude Code executes automatically at a specific point in the agent's lifecycle — independent of what Claude itself would choose to do. The distinction is worth stating plainly, since it's exactly the design principle behind having both mechanisms: settings and hooks are enforced by the client regardless of what Claude decides; CLAUDE.md instructions shape Claude's behavior but are not a hard enforcement layer.

Hooks attach to named lifecycle events. The two you'll reach for most:

  • PreToolUse — fires before a tool call executes; can inspect the proposed call and block it.
  • PostToolUse — fires after a tool call succeeds; good for validation, linting, or logging after the fact.

(Claude Code exposes many more — SessionStart, Stop, UserPromptSubmit, PreCompact/PostCompact, SubagentStart/SubagentStop, and others — but PreToolUse/PostToolUse cover the majority of real guardrail use cases.)

Hooks are configured in settings.json under a hooks key, matched to specific tools by name or regex:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-destructive.sh"
          }
        ]
      }
    ]
  }
}

The hook script receives structured JSON on stdin — including tool_name and tool_input — and communicates its decision back through its exit code and, optionally, JSON on stdout:

#!/bin/bash
COMMAND=$(jq -r '.tool_input.command')

if echo "$COMMAND" | grep -q 'rm -rf'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Destructive command blocked by hook"
    }
  }'
else
  exit 0   # no opinion — normal permission flow (allow/deny/ask) applies
fi

Exit code 0 means proceed (optionally with a JSON decision); exit code 2 is a blocking error — the action is stopped unless the hook's JSON output explicitly overrides that. Because the script is deterministic code, not a language model, it will catch rm -rf in tool_input.command every single time, with zero probability of the agent talking itself into an exception under time pressure or an unusual prompt.

When to reach for a hook instead of a CLAUDE.md rule: if the instruction needs to run at a fixed point regardless of what Claude decides — before every commit, after every file edit, blocking a specific category of command outright — write it as a hook. If it's guidance that should generally shape judgment but doesn't need airtight enforcement — "prefer composition over inheritance," "check existing utils before adding a new dependency" — CLAUDE.md is the right layer. A useful mental model: CLAUDE.md is what you'd tell a new hire in their onboarding doc; hooks are what you'd put in a pre-commit check or a CI gate, because you don't trust onboarding docs alone to prevent an incident.


The .claude Directory: A Map

Everything this subject has covered so far — CLAUDE.md, .claude/rules/, settings.json, hooks, auto memory — lives inside a larger, consistent structure: a project-level .claude/ directory (plus a few root-level files) mirrored by a personal ~/.claude/ directory that applies across every project on your machine. Most users only ever touch two files in it — CLAUDE.md and settings.json — but it helps to know the rest of the map exists, both so you recognize files you encounter in other people's repos and so you know where to look when you need something beyond the basics.

At the project root (not inside .claude/):

File Committed? Purpose
CLAUDE.md Yes Project instructions — covered above
.mcp.json Yes Project-scoped MCP server definitions, shared with the team
.worktreeinclude Yes Gitignored files (e.g. .env) to copy into new git worktrees, using .gitignore-style patterns

Inside .claude/ at the project level:

File / folder Committed? Purpose
settings.json Yes Permissions, hooks, statusLine, model, env, outputStyle — enforced configuration, not advisory context
settings.local.json No (gitignored) Your personal overrides on top of the team's settings.json
rules/*.md Yes Topic-scoped instructions, optionally gated to matching files with paths: frontmatter — covered above
skills/<name>/SKILL.md Yes Reusable prompts invoked with /name, or auto-invoked by Claude when relevant; can bundle supporting files (templates, reference docs, scripts) alongside the prompt
commands/*.md Yes The older, single-file predecessor to skills — still supported, but new work should generally use skills/ instead, since a skill is the same /name invocation plus the ability to bundle files
output-styles/*.md Yes Project-shared output styles, if a team wants one everyone uses (most output styles are personal and live in the global directory instead)
agents/*.md Yes Subagent definitions — their own system prompt, tool restrictions, and optionally their own model
workflows/*.js Yes Saved dynamic workflow scripts; each file becomes its own /<name> command
agent-memory/<name>/ Yes Persistent memory for a specific subagent — a distinct feature from your main session's auto memory, covered above

The global counterpart, ~/.claude/, mirrors most of this structure — CLAUDE.md, settings.json, rules/, skills/, commands/, output-styles/, agents/, workflows/, agent-memory/ — but scoped to you, across every project, and never committed anywhere. It also holds a few things with no project-level equivalent: keybindings.json (terminal keyboard shortcuts), themes/ (custom color themes), and — most relevant to this subject — projects/<project>/memory/, which is where auto memory actually lives on disk, one directory per project, as described in the previous section.

Two files sit adjacent to but outside this structure, worth knowing about even though you'll rarely edit them directly: managed-settings.json (deployed at a system-level location by IT/MDM — the same managed policy tier CLAUDE.md has) and ~/.claude.json, which holds application state rather than settings — OAuth session, per-project trust decisions, UI toggles, and your personal MCP servers (as opposed to the team-shared ones in .mcp.json).

A security-relevant note on the rest of ~/.claude/: beyond the config you author, the directory also accumulates application data as you work — full session transcripts (projects/<project>/<session>.jsonl), pre-edit file snapshots used for checkpoint restore, prompt history, and more. This data is plaintext, not encrypted at rest. If a command prints a credential or Claude reads a .env file during a session, that value is written to the transcript on disk. Most of this data ages out automatically (governed by the same cleanupPeriodDays setting mentioned above), but auto memory is deliberately exempt from that sweep, for the durability reasons already covered. If you need to fully clear a project's local state — transcripts and auto memory included — claude project purge <path> does that, printing a full deletion plan and asking for confirmation before removing anything.


Choosing the Right Mechanism: A Decision Guide

By this point in the subject, five overlapping-sounding mechanisms have all been introduced: CLAUDE.md, .claude/rules/, auto memory, settings.json permissions, and hooks. In practice, picking the right one comes down to two questions: who should write this down, and how strong does the guarantee need to be?

Mechanism Who writes it Enforcement Best for
CLAUDE.md You, reviewed like code Advisory — Claude reads it and tries to follow it Team-shared conventions and surprises that apply to every session, regardless of what file is being touched
.claude/rules/ You, reviewed like code Advisory, same as CLAUDE.md The same kind of content as CLAUDE.md, but scoped to a file type or directory via paths: — so it only enters context when actually relevant, keeping the always-loaded root file lean
Auto memory Claude (from your corrections or its own observations) Advisory, same as CLAUDE.md Personal, discovered, or provisional knowledge — debugging insights, build quirks, one-off preferences — that doesn't need team review or version control
settings.json permissions You Enforced by the client Controlling which tool calls run unattended — not behavior, but access
Hooks You (a script) Enforced by the client, deterministically Anything that must happen (or must never happen) at a fixed lifecycle point, independent of what Claude decides

A worked sequence of questions gets you to the right cell most of the time:

  1. Does this need to be true every session, for everyone on the team, regardless of which files are touched? → CLAUDE.md.
  2. Is it the same kind of team-authored guidance, but only relevant to a subset of files or a subdirectory?.claude/rules/ with paths: frontmatter, so it doesn't tax every session's starting context.
  3. Is it something Claude figured out, or a personal correction that doesn't need to be reviewed or shared via git? → let it go to auto memory (which it will, by default) rather than asking for it to be added to CLAUDE.md.
  4. Is it about restricting what runs without a prompt, not about shaping behavior?settings.json permissions.
  5. Does getting it wrong even once carry real cost, and is there a cheap mechanical check for it? → a hook, layered on top of whichever advisory mechanism also documents the why for Claude's own judgment.

One thing that doesn't belong in any of the five: content Claude would derive correctly by reading the code for a few minutes. That's true whether it's written as a CLAUDE.md paragraph, a rule, or an auto-memory note — the "would a competent engineer get this wrong just by reading the code?" test from earlier in this subject applies uniformly across every one of these mechanisms, not just CLAUDE.md.


Worked Example: Designing a CLAUDE.md for a Mid-Size Repo

Consider a hypothetical repo, aperture — a Next.js frontend, a FastAPI backend, a Celery worker, and a Postgres database, with about 40 engineers touching it. Here's a first draft, written the way most teams write their first CLAUDE.md:

# Aperture

Aperture is a SaaS analytics platform. The frontend is built in Next.js
with TypeScript and Tailwind CSS. The backend is a FastAPI application
written in Python 3.11, using SQLAlchemy as the ORM and Postgres 15 as
the database. Background jobs run via Celery with Redis as the broker.

## Directory structure

- `/frontend` — Next.js app
  - `/frontend/components` — React components
  - `/frontend/pages` — Next.js pages
  - `/frontend/lib` — shared utilities
- `/backend` — FastAPI app
  - `/backend/api` — route handlers
  - `/backend/models` — SQLAlchemy models
  - `/backend/services` — business logic
- `/worker` — Celery tasks

## Dependencies

Frontend: react, next, tailwindcss, axios, zod, react-query...
Backend: fastapi, sqlalchemy, celery, redis, pydantic, alembic...

## Code style

Please write clean, readable, well-documented code. Follow best
practices. Use meaningful variable names. Keep functions small.
Add comments where helpful. Test your changes before committing.

Critique: every sentence here is either something Claude would infer in the first thirty seconds of reading the repo (the directory layout, the dependency list, the language and frameworks — all visible in package.json and pyproject.toml) or vague enough to be unverifiable ("clean, readable," "best practices," "meaningful names"). None of it is a surprise. None of it would prevent a real mistake. It's documentation, not instruction — and at ~35 lines it's not even egregiously bloated yet, but it's already 100% filler, which means the next thing added to it (a real, load-bearing rule) has zero peers to compete with for attention, and the file will likely keep growing in this same unhelpful direction.

Here's a refined version, focused entirely on what would actually surprise an agent working in this repo:

# Aperture

## Commands
- Backend tests: `make test-backend` (spins up a throwaway Postgres via
  docker compose — do NOT run pytest directly, fixtures depend on it)
- Frontend tests: `pnpm test` (NOT npm — the lockfile is pnpm-only,
  npm install will desync it)
- Full local stack: `make dev` (starts frontend, backend, worker, redis)
- Migrations: `alembic revision --autogenerate -m "..."`, then review
  the generated file by hand — autogenerate misses index changes often

## Conventions that differ from framework defaults
- Soft deletes only. Every table has a `deleted_at` column; never write
  a hard DELETE against a user-facing table. Use `Model.soft_delete()`.
- API responses use camelCase (frontend convention), but the DB and
  Python code are snake_case. Serialization happens in
  `backend/api/serializers.py` — don't hand-roll case conversion
  elsewhere.
- Feature flags are checked via `flags.is_enabled(name, user)`, not
  environment variables. `backend/flags/` is the source of truth.

## Known traps
- `backend/legacy_billing/` is unmaintained, kept only for one
  enterprise client's contract obligations. Do not extend it or use it
  as a reference for new billing code — see `backend/billing/` instead.
- The Celery worker retries silently on `TransientError` up to 5 times.
  If a task "isn't running," check retry logs before assuming it never
  fired.
- CI requires Conventional Commits (`feat:`, `fix:`, `chore:`); a
  malformed commit message fails the merge gate, not just a lint step.

At roughly the same length, this version says almost nothing Claude could have derived by reading the code — it's entirely composed of commands with non-obvious flags, deviations from what a framework would lead you to assume, and traps that have presumably already bitten someone once. That's the bar: if you can imagine a competent engineer confidently doing the wrong thing without this line, it earns its place.


Common Mistakes

CLAUDE.md as a documentation dump. Covered above — the most common failure mode. If you're tempted to add a sentence describing what the codebase already makes obvious, don't; that sentence is diluting the entries that matter.

Stale instructions after a refactor. A CLAUDE.md that says "auth logic lives in middleware/auth.py" silently becomes actively harmful once that code moves to services/auth/. Unlike a broken build or a failing test, a stale CLAUDE.md entry doesn't announce itself — it just quietly sends the agent to the wrong place, confidently. Treat CLAUDE.md like any other artifact that can rot: review it during major refactors, and prefer instructions that reference stable concepts ("auth logic") only when paired with a path you're committed to keeping current, or better, verified periodically rather than assumed correct forever.

Over-broad permissions in settings.json. Allowing Bash(*) or Bash(git *) to eliminate approval prompts also removes the one point where a human would have caught a dangerous command before it ran. Permissions should be scoped to the specific commands you've decided are safe to run unattended, not to the category of tool that happens to contain them. deny should be used explicitly for genuinely destructive operations, not relied upon implicitly by simply never adding them to allow — remember that permissions merge across scopes, so a deny set once at the project level protects every engineer, including new hires who haven't yet learned what not to allow.

Conflicting instructions across layered files. Because managed, user, project, and local CLAUDE.md files are concatenated rather than resolved, two files with contradictory guidance don't produce a well-defined outcome — Claude may pick either one. This gets worse the more scopes a team uses. Periodically audit, especially after a project CLAUDE.md and a personal user-level CLAUDE.md have both existed for a while.

Treating CLAUDE.md as enforcement. Even a well-written CLAUDE.md is advisory context — it shapes behavior, it doesn't guarantee it. If a rule must never be violated (secrets must never be committed, a specific table must never receive a raw DELETE), that rule belongs in a hook or in permissions.deny, not solely in prose the agent is trying, but not guaranteed, to follow.

Treating auto memory as team knowledge. Auto memory lives at ~/.claude/projects/<project>/memory/ — outside the repo, outside version control, machine-local by construction. A genuinely useful discovery Claude made ("the staging DB needs a manual VACUUM after large migrations, or writes stall") is only visible to you, on this machine, until someone promotes it to CLAUDE.md. Teams that never audit their auto memory tend to end up with the same hard-won lesson rediscovered independently on every engineer's laptop. Periodically skim MEMORY.md (via /memory) for entries that should graduate to a committed CLAUDE.md or rule, the same way you'd periodically audit CLAUDE.md itself for staleness.


Summary

Mechanism Written by Scope Enforcement Use for
CLAUDE.md You (team, via git) Managed / user / project / local, concatenated Advisory — shapes behavior Surprises, conventions, non-obvious commands, every session
.claude/rules/ You (team, via git) Same scopes as CLAUDE.md; optionally gated by paths: Advisory — same as CLAUDE.md The same kind of content as CLAUDE.md, scoped to a file type or directory so it doesn't tax every session
Auto memory Claude Per-repo, machine-local (~/.claude/projects/<project>/memory/) Advisory — same as CLAUDE.md Discovered quirks, stated preferences, personal notes that don't need review
settings.json permissions You Managed / local / project / user, merged Enforced by the client Scoping which tool calls run unattended
Hooks You Configured in settings.json Enforced deterministically Guardrails that must hold every time

For the full inventory of files these mechanisms live in — including the ones this subject only touches briefly, like skills, subagent definitions, and saved workflows — see the .claude directory tour above.

Before adding to a CLAUDE.md, ask: would a competent engineer get this wrong by just reading the code? If not, leave it out. Before broadening a permission, ask: what's the blast radius if this exact pattern matches a command I didn't anticipate? Before relying on an instruction to prevent a serious mistake, ask: is this important enough that it needs a hook instead of a sentence? Answering these consistently — and revisiting the answers as the codebase changes — is most of what it takes to keep an agent well-configured over the life of a real project.

Ready to test your knowledge?

Practice questions

We use cookies for product analytics to improve OmniAtlas. See our Privacy Policy.