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.