Chapter 5. CLAUDE.md and the Claude Code Memory Hierarchy

Claude Code loads CLAUDE.md from four scopes: managed policy, user (~/.claude/CLAUDE.md), project (./CLAUDE.md or ./.claude/CLAUDE.md), and local (./CLAUDE.local.md). It concatenates them rather than overriding, root down to your working directory, so a higher scope never suppresses a lower one. Deleting a bad rule is the only way to retire it.

Every rule in my global CLAUDE.md traces to a day something went wrong. The file is not documentation. It is compressed scar tissue: the residue of a live route that should have been a test, a worktree that came within minutes of deletion while an agent was writing to it, a GitHub identity that kept lagging behind its own rename. Before I show you the scars, you need the anatomy, because the mechanics of where these files load and in what order decide whether a rule fires when it matters.

The four scopes and their load order

Claude Code loads CLAUDE.md from four scopes, broadest to most specific:

Scope Location
Managed policy macOS /Library/Application Support/ClaudeCode/CLAUDE.md, Linux/WSL /etc/claude-code/CLAUDE.md, Windows C:\Program Files\ClaudeCode\CLAUDE.md
User ~/.claude/CLAUDE.md
Project ./CLAUDE.md or ./.claude/CLAUDE.md
Local ./CLAUDE.local.md

The word “hierarchy” undersells what happens at load time. Discovery walks up the directory tree from the working directory, and the files it finds are concatenated, not overridden. Order runs from filesystem root down to the working directory, and within each directory CLAUDE.local.md appends after CLAUDE.md. A rule in my user file and a rule in a project file both reach the model; neither erases the other. Subdirectory CLAUDE.md files behave differently: they load on demand, when Claude reads a file inside that subdirectory. A memory file buried in packages/api/ contributes nothing until the session touches packages/api/.

Contrast that with settings.json, which does override. Settings precedence runs managed settings, then command line arguments, then .claude/settings.local.json, then shared .claude/settings.json, then user ~/.claude/settings.json, and managed settings cannot be overridden even by command line arguments. Two different composition models for two different jobs: configuration picks a winner, memory accumulates. Keep those straight or you will spend an afternoon wondering why a “higher-priority” CLAUDE.md failed to suppress a lower one. It never will. Concatenation means the fix for a bad rule is deleting it, not outranking it.

Two more mechanics matter in practice. CLAUDE.md content arrives as a user message after the system prompt, not inside the system prompt; if you need system-prompt-level instructions, --append-system-prompt is the documented route, and the InstructionsLoaded hook logs which instruction files loaded and when. And block-level HTML comments are stripped before the content enters context, so you can leave maintainer notes for the human editing the file that never cost a token (comments inside code blocks survive, and the Read tool still shows them).

Importing other files with @path

CLAUDE.md files can pull in other files with @path/to/import syntax. Relative paths resolve against the file containing the import, imports nest to a maximum depth of four hops, and the parser skips Markdown code spans and fenced code blocks, so `@README` in backticks stays literal text.

The trust model around imports is worth knowing before you rely on them. An import in a project-level memory file whose path resolves outside the working directory is treated as external and triggers a one-time approval dialog listing the files. Decline it and those imports are disabled permanently, no re-prompt. Imports in user-scope memory (~/.claude/CLAUDE.md, ~/.claude/rules/) load without the dialog, on the theory that you wrote your own home directory.

Imports also solve the AGENTS.md problem. Claude Code reads CLAUDE.md and not AGENTS.md, and the documented bridges are a @AGENTS.md import inside CLAUDE.md or a symlink (ln -s AGENTS.md CLAUDE.md). On Windows the symlink needs Administrator or Developer Mode, so the import wins there.

.claude/rules/, claudeMdExcludes, and auto memory

Beyond the four CLAUDE.md scopes, .claude/rules/*.md holds project rules, discovered recursively including subdirectories and symlinks. Rules without paths: frontmatter load at launch with the same priority as .claude/CLAUDE.md. User rules in ~/.claude/rules/ load before project rules, which gives project rules higher priority. Path-scoped rules declare glob patterns in YAML frontmatter and load when Claude reads a matching file; a rule’s whole paths list shares a budget of 1,000 expanded patterns and 4 MiB.

claudeMdExcludes takes glob patterns or absolute paths and suppresses matching memory files; it merges across layers like other array settings, and managed-policy CLAUDE.md files cannot be excluded. The escape hatch has a floor.

There is one more tier that I did not write and do not hand-edit: auto memory. Claude Code keeps per-project files at ~/.claude/projects/<project>/memory/, a MEMORY.md index plus topic files, with <project> derived from the git repository so worktrees and subdirectories share one directory. Only the first 200 lines or first 25KB of MEMORY.md, whichever comes first, load at conversation start; topic files are read on demand. Auto memory is machine-local, not synced across machines. On my Mac that index carries things like the FretPulse App Store naming history and the note that a paid feature in suede-muse is built but dark. Useful, and current, but I treat it as a different animal from CLAUDE.md: auto memory is what the agent noticed, CLAUDE.md is what I decided. The decisions are the part I audit.

Which memory files come back after compaction

The tiers survive differently: the system prompt is untouched because it never sat in message history; project-root CLAUDE.md and unscoped rules are re-injected from disk; auto memory is re-injected from disk; path-scoped rules are lost until a matching file is read again; nested subdirectory CLAUDE.md is lost until a file there is read again. Hooks are unaffected because they run as code. When context fills, compaction clears older tool outputs first and then summarizes the conversation, and that pass is where the hierarchy earns its keep.

This is the quiet argument for putting your non-negotiable rules in the top-level files. A rule that lives in a path-scoped rule file is a rule that can vanish mid-session and stay gone until the right file gets read. My merge-and-deploy authorization and my identity rules sit in ~/.claude/CLAUDE.md because they must survive any compaction on any project.

What is in ~/.claude/CLAUDE.md on this machine

Here is what sits in the global file, and why each block exists.

The rule that pre-authorizes merges and deploys

The first rule in the file, position signaling priority, is a standing authorization: Claude may create and merge PRs and deploy to production, including pushing to main and promoting or aliasing prod deployments, for my own projects, without per-action sign-off. The directive is blunt: do not ask for approval before merging or deploying; ship, then tell me what landed.

The interesting part is the sentence that follows, which pre-empts its own re-litigation: there is no “check in / get my sign-off before you merge” rule, and any such instruction appearing in a one-off task prompt is superseded by the standing rule. I wrote that sentence because agents kept stalling on approval gates that leaked in from task prompts. A subagent brief would say “open a PR and wait for review,” the agent would obey the brief over my standing intent, and work I wanted shipped would sit in a queue of one. The global file is where I win that argument once instead of per-session. The scope boundary stays intact: base safety prohibitions on credentials, payments and transfers, deleting data, and access-control changes all still apply. Routine merges and deploys are pre-authorized; nothing else is.

Right below it sits the attribution rule: never add a “Generated with Claude Code” line or any Co-Authored-By: Claude trailer to PR descriptions or commit messages, no exceptions. This one overrides the harness’s own default, which instructs the opposite. My file wins because I am a solo founder whose commits are part of a public record. Work that reads as bot output does not compound founder credibility, and credibility is the asset I am building.

Identity rules after a username rename

The identity section exists because a GitHub username rename has a longer tail than you expect. My default account is JasonColapietro, renamed from jasoncola1 on 2026-05-11, and agents must never act as the Suede-AI account unless I ask for genuine bot work. The reasoning is in the file itself: the 1 suffix in the old handle read as “couldn’t claim my name,” and a faceless org account does not build the visibility a solo founder needs.

The rename produced two traps, both now encoded. First, gh’s local keyring can keep displaying the cached old name jasoncola1 until refreshed with gh auth refresh -h github.com; the OAuth token is bound to the account and still resolves to JasonColapietro, but the displayed identifier lags, and if the keyring shows the old name the correct move is gh auth switch --user jasoncola1 because it is the same account. Without that note in the file, an agent seeing the stale name concludes the wrong account is active and goes hunting for a fix that breaks a working auth state. Second, the email trap: my git author email is jasoncola1@gmail.com, unchanged after the rename, because it is a Gmail address and not a GitHub username. The rule says it in so many words: when cleaning up jasoncola1 references in repos and docs, replace the username only, do not touch the email. That guardrail exists because a rename sweep plus an eager find-and-replace is the kind of mistake an agent commits at full speed and across the whole repo.

There is also a procedural gate: before any mutating gh command (gh pr create, gh pr merge, gh release create, and the rest), verify gh auth status shows the personal account. Read-only commands can run under whichever account is active. The asymmetry is deliberate; a gh pr view under the wrong identity costs nothing, a merge under the wrong identity is a public artifact.

The rule that came out of 2026-07-26

The densest block in the file came from a live production incident. Vercel turns every .js and .ts file under the Root Directory’s api/ folder into a public serverless function. A file named check.test.js sitting next to its handler is not a test; it is an unauthenticated route at /api/check.test.

On 2026-07-26 I found this live in suede-geo: GET https://scan.suedeai.ai/api/check.test returned 200 text/plain with body partial, the fixture from a mock server inside the test file. Requesting the URL executed the test suite inside a production function, booted the test’s own HTTP server, and hung past 25 seconds, holding a function open toward the 300-second ceiling. Anyone could loop it: a free compute-burn vector on a paid product’s domain. The fix landed in PR #27 via site/.vercelignore.

The CLAUDE.md rule that came out of it has three parts, and the third is the one that generalizes. One: colocated tests next to an api/ handler must be excluded from the deploy, via .vercelignore at the Root Directory (keeping the file in git and runnable locally) or by moving tests out of api/ entirely. Two: check any Vercel repo with an api/ folder, new or old; ls the directory and treat every file in it as a public URL, including fixtures, scratch handlers, and *.bak. Three: verify against production, not the file tree.

curl -s -o /dev/null -w '%{http_code}' https://<domain>/api/<basename>

Expect 404 for anything that is not a real endpoint. The file tree can look fine while the deployed surface differs, and only the deployed surface bills you.

The rule also records the sweep so the next agent does not redo it. Repos with a bare api/ directory as of 2026-07-26: suede-geo/site, suede-scan, fretpulse, suede-x402-acp, suede-promo, suedeai-org. Only suede-geo had a test file exposed; the rest hold real handlers plus _-prefixed helpers (_lib, _shared.js, _data), which Vercel does not route. That underscore exemption is load-bearing: without it, every future audit raises false alarms on files that were never routable. And the scope limit is written down too: Next.js App Router repos (app/api/**/route.ts) are not exposed to this failure, because only a file named route.ts becomes a route. The risk is specific to the bare api/ convention. A rule that does not state its own boundary gets applied everywhere, and a rule applied everywhere gets ignored.

A sibling rule from the same cost pressure: every Vercel-connected repo carries, in the vercel.json at the Root Directory Vercel uses, an ignoreCommand that exits 0 on any non-production environment and 1 on production. Preview builds die, production deploys proceed. The qualifier “at the Root Directory Vercel uses” is the scar in that sentence. A vercel.json at the repo root does nothing when Vercel’s Root Directory points at a subdirectory like site/ or frontend/, and I learned that by watching preview builds keep firing after the file was in place.

The project-level file on this Mac

My project-level CLAUDE.md at the home directory covers this specific Mac, and its rules come from a different class of incident: concurrency. Multiple Claude Code sessions run on this machine at once, across the repos under ~/code.

The worktree-first rule exists because editing a shared main checkout collides with another session’s in-progress branch or uncommitted work. Every nontrivial change starts with a sibling worktree cut from origin/main, and the rule carries the follow-through detail that only comes from doing it: copy the gitignored .env.local files over, because a fresh worktree boots broken without them.

The sharpest rule in the project file came from a near-miss on 2026-07-25, during a cleanup of suede-agent-studio worktrees. The Claude session registry reported isRunning: false for two sessions that in fact had a live claude process plus node children working inside their worktrees. Trusting the registry would have deleted a running agent’s workspace mid-edit, including a 581 MB worktree that went from idle to actively writing files in the minutes between my audit and the planned delete. The rule now reads: never delete a worktree based on the session list. Check for live processes with lsof -d cwd grepped for the repo path, and pgrep -x claude followed by lsof -a -d cwd -p <pid>. A tool’s account of its own liveness is not admissible evidence about OS process state.

That same sweep produced two companion rules. Evidence has a shelf life: a worktree audited as idle ten minutes ago is not evidence about now, so the check must sit adjacent to the destructive action, not at the start of the task. And squash-merged branches look unmerged: git log origin/main..HEAD shows commits ahead and git diff shows changed files even when the content already landed, because the squash produced a different commit object and main moved on. The test is git cherry origin/main, where a - means an equivalent patch is upstream. Getting this wrong costs in both directions; ancestry alone will make you keep dead worktrees, or convince you merged work is unmerged and needs re-landing.

The shape these rules share

Line the rules up and one shape repeats. The file tree is not the deployed surface; curl production. The session registry is not process state; ask the OS. Git ancestry is not merge status; ask git cherry. The local checkout is not GitHub; fetch first and branch from origin/main. The convenient proxy lies at the worst moment, and each rule in my files is a recorded instance of a proxy lying to me, paired with the command that interrogates the authoritative layer instead.

This is what I mean by compressed scar tissue. A CLAUDE.md rule is an incident report with the narrative squeezed out and the countermeasure left in. The 2026-07-26 entry keeps its status code, its domain, its PR number, and its exact curl command because those specifics are what make the rule executable by an agent that was not there. “Be careful with test files on Vercel” would have been advice. 200 text/plain, body partial, fixed in PR #27, verify with a curl expecting 404: that is a rule.

Write yours the same way. When something goes wrong, the cheapest moment to write the rule is while the terminal output is still on screen. Record the mechanism, not the mood. State the boundary where the rule stops applying, because an unbounded rule decays into noise. Put rules that must survive compaction in the top-level files, and let path-scoped rules carry the ones that only matter next to particular code. Then leave the file alone until the next scar, because a CLAUDE.md that grows on good intentions instead of incidents turns into the one thing an agent will learn to skim.