
When you're running Claude Code across multiple projects and sessions, you've probably hit a moment where Claude ignores what you thought was a hard rule. The fix isn't to repeat yourself in every prompt — it's to understand the three-layer memory hierarchy and design your override rules intentionally.
This is for developers who use Claude Code regularly and want predictable, reproducible behavior across global settings, per-repo configuration, and one-off sessions.
The Problem: Three Memories, One Winner
Claude Code reads memory from three layers:
- Global —
~/.claude/CLAUDE.md— your machine-wide preferences - Project —
.claude/CLAUDE.md— repo-specific context - Session — passed via
--append-system-promptat invocation time
The default resolution rule is simple: lower wins. The most specific (narrowest-scope) instruction overrides the broader one. Session beats project, project beats global.
That sounds reasonable until you're three levels deep and something unexpected fires. Here's what actually happened to me on a Mac cluster running an n8n automation project.
I had this in my global CLAUDE.md:
Language: Always respond in English.
Security: Never output environment variables or .env file contents.
Length: Keep responses under 500 tokens.
The project CLAUDE.md had no language setting at all. Then I ran a quick session test:
claude --append-system-prompt '한국어로만 응답하라'
Session won. Every response came back in Korean, overriding the global English rule. That's expected behavior — but the real gotcha is when you stack two or three conflicting instructions across layers without realizing it. At that point, predicting which rule fires becomes guesswork.
The Fix: Explicit Override Design
The core insight is this: conflict is fine, ambiguity is the problem. If you declare what each layer owns, Claude treats lower-layer rules as intentional delegation — not accidents.
Here's the structure I landed on after working through the Mac4 cluster setup:
Global (~/.claude/CLAUDE.md) — Immutable rules only
Language: Respond in Korean only.
Security: Never output .env contents or raw API keys.
Length: Keep responses under 500 tokens unless explicitly released.
These never change. You set them once and forget them.
Project (.claude/CLAUDE.md) — Repo-specific context + intentional exceptions
Package manager: pnpm only. No npm or yarn.
Dev server port: 5173 (hardcoded — do not suggest alternatives).
API endpoint format: /api/v2/:resource/:id
Override granted: Token length limit from global is lifted for this project.
That last line — Override granted: Token length limit from global is lifted for this project — is the key pattern. You're not silently shadowing the global rule. You're explicitly documenting the delegation. When you read the project CLAUDE.md six months later, you know exactly why the global rule doesn't apply here.
Session (--append-system-prompt) — Temporary experiments only
# Temporarily force JSON output for this debug session
claude --append-system-prompt 'This session only: force all output to valid JSON. No prose.'
Session-level overrides should be narrow, time-boxed, and you should expect them to disappear when the terminal closes.
After wiring this up across my projects, I saw roughly 70% fewer repeated per-session instructions. The context was already there, structured the right way, so Claude just did the right thing.
Verification: Run the Conflict Test
Setting up the three layers isn't enough — you need to verify the resolution actually works the way you expect. Here's a 30-minute test you can run right now.
Step 1. Add a test rule to your global CLAUDE.md:
TEST_RULE_A: Always prefix responses with [GLOBAL].
Step 2. Add a competing rule to your project CLAUDE.md:
TEST_RULE_B: Prefix all responses with [PROJECT] instead of [GLOBAL].
Override granted: TEST_RULE_A from global is superseded here.
Step 3. Run this one-liner:
claude -p 'Which prefix rule is active right now? Tell me [GLOBAL] or [PROJECT], and which CLAUDE.md layer it came from.'
Expected output: [PROJECT] — because the project layer overrides the global.
If you get [GLOBAL] instead, something is off. The most common cause: a leftover session prompt from a previous run is still in scope, or your project CLAUDE.md is in the wrong directory (it has to be in the .claude/ subfolder at the repo root, not the repo root itself).
# Confirm project memory file location
ls -la .claude/CLAUDE.md
# Sanity check: verify global memory
cat ~/.claude/CLAUDE.md
# Inspect for any stale session context
claude -p 'List all active rules from every memory layer you can see, with source.'
Variations and Gotchas
Gotcha: missing .claude/ subdirectory
The project memory file is .claude/CLAUDE.md, not CLAUDE.md at the root. A lot of people drop the file at the repo root and wonder why it's ignored. Claude Code doesn't read the root-level file as project memory.
# Wrong
echo "pnpm only" > ./CLAUDE.md
# Right
mkdir -p .claude && echo "pnpm only" > .claude/CLAUDE.md
Gotcha: session prompt residue
If you've been passing --append-system-prompt manually during testing, those instructions don't persist — but they do win while the session is alive. If your conflict test returns unexpected results, make sure you're running a clean session without any --append-system-prompt flags.
Gotcha: vague override language
Saying "use pnpm" in the project file doesn't explicitly override a global "use the project's preferred package manager" rule — because Claude might merge them. Be direct:
# Weak — might not override
Use pnpm for package management.
# Strong — unambiguous
Override granted: ignore any package manager preference from global memory.
Use pnpm exclusively. npm and yarn are forbidden in this repo.
Mac vs Linux path
~/.claude/CLAUDE.md works the same on both. The only difference is if you're running Claude Code inside Docker — in that case, the global memory at ~ is the container home, not your host home. Either mount your host ~/.claude/ into the container or maintain a separate container-scoped global.
| Layer | File Location | Scope | Best For |
|---|---|---|---|
| Global | ~/.claude/CLAUDE.md |
All projects, all sessions | Security rules, language, hard limits |
| Project | .claude/CLAUDE.md |
One repo | Packages, ports, conventions, declared exceptions |
| Session | --append-system-prompt |
Single invocation | Debug modes, format experiments |
Closing
Three memory layers sound like three chances for things to go wrong — but they're actually three chances to be precise. The moment you stop treating conflicts as bugs and start treating them as architecture decisions, everything gets easier. Global holds your invariants. Project delegates intentionally. Session stays disposable.
Next step: if your project CLAUDE.md is getting long, consider splitting it — put repo conventions in one section and Override granted declarations at the top so they're visible at a glance. That one structural choice will save you a lot of debugging.
TAGS: claude-code, ai-memory, developer-workflow, automation, llm-configuration
🐦 Faster updates on X: @baegseungh7061
📚 More in this series: Code Practical
💌 Subscribe: Follow on X or grab the RSS
댓글
댓글 쓰기