Writing Context and Persistent Memory
In our previous article on context engineering, we established that an AI agent is only as good as the context it receives. When you open Claude Code and start a conversation, Claude's context window begins almost empty — it knows nothing about your project's architecture, coding conventions, test commands, or deployment workflow. Every piece of project-specific knowledge must be loaded into context before Claude can act on it.
The first and most powerful strategy for shaping that context is writing it down. Claude Code provides a layered memory system built around special files called CLAUDE.md files. These files act as persistent instructions that are automatically loaded into Claude's context at the start of every session. Think of them as a briefing document that your AI agent reads before it begins any work.
This article explains the full memory hierarchy in Claude Code — from project-level instructions to user preferences to dynamic file imports — and shows you how to automate context injection using hooks. By the end, you will know how to set up a memory system that keeps Claude consistently informed, session after session.
The Memory Hierarchy
Two Systems: CLAUDE.md vs. Auto Memory
Claude Code has two distinct systems for remembering information:
- CLAUDE.md files — Instructions that you write and maintain manually. They are loaded into context at the start of every session. You have full control over their content.
- Auto memory — Notes that Claude creates and maintains on its own (or when you use the
/memorycommand). These are stored in a separate location and are also loaded at session start.
For this article, we focus primarily on CLAUDE.md files because they give you explicit, version-controlled instructions. Auto memory is covered at the end as a complementary feature.
The Three-Tier Memory Model
CLAUDE.md files exist at three levels, forming a hierarchy that flows from broad to specific:
| Tier | File Location | Purpose | Shared via Git? |
|---|---|---|---|
| Project Memory | ./CLAUDE.md (project root) | Project-wide coding standards, commands, architecture | Yes |
| User Memory | ~/.claude/CLAUDE.md | Personal preferences that apply to all your projects | No |
| Local Memory | ./CLAUDE.local.md (project root) | Personal overrides for a specific project | No (gitignored) |
When Claude Code starts a session, it walks up the directory tree from your current working directory, finds all CLAUDE.md files, and concatenates them together. At each directory level, CLAUDE.local.md is appended after CLAUDE.md. The result is a single block of instructions that Claude reads before processing your first prompt.
How the Load Order Works
Imagine you are working inside ~/projects/myapp/src/. Claude Code will look for CLAUDE.md files in this order:
~/.claude/CLAUDE.md(user-level, always loaded)~/projects/CLAUDE.md+~/projects/CLAUDE.local.md~/projects/myapp/CLAUDE.md+~/projects/myapp/CLAUDE.local.md~/projects/myapp/src/CLAUDE.md+~/projects/myapp/src/CLAUDE.local.md
All found files are concatenated and loaded into context. This means you can have general rules at the top level and more specific rules in subdirectories. The combined content should stay under 200 lines for best results — Claude reads all of it, but overly long files can dilute the most important instructions.
Project Memory: The CLAUDE.md File
What Goes in a Project CLAUDE.md?
The project-level CLAUDE.md is the most important memory file. It lives in your project root and is committed to version control so that every team member (and every CI/CD pipeline running Claude) gets the same instructions.
A good project CLAUDE.md typically includes:
- Build and test commands — How to install dependencies, run tests, lint, and build
- Code style conventions — Formatting rules, naming conventions, import ordering
- Architecture overview — Key directories, important files, how modules connect
- Workflow rules — Branch naming, commit message format, PR process
- Do's and don'ts — Common mistakes to avoid, patterns to prefer
Quick Start: The /init Command
If you are starting fresh, Claude Code can generate a starter CLAUDE.md for you. Run:
Claude will scan your project's file structure, package files, and configuration, then generate a CLAUDE.md with reasonable defaults. You should always review and refine the output — the generated file is a starting point, not a final product.
Example: A Real Project CLAUDE.md
Here is a practical example for a Node.js project using Express:
Notice the structure: short sections, bullet points, specific commands. Claude can scan this in milliseconds and immediately know how to build, test, and follow conventions in this project.
Writing Effective Instructions
The official Claude Code documentation offers clear advice on writing good CLAUDE.md files:
- Be specific, not vague — Write
Use vitest, not jestinstead ofUse the right testing framework. - Be concise — Use short phrases and bullet points. Aim for under 200 lines total.
- Use Markdown headers and lists — Structure helps Claude parse the content quickly.
- Check it into version control — The project CLAUDE.md should be a shared team resource.
- Prune regularly — Remove rules that are no longer relevant. Stale instructions confuse the model.
- Test your instructions — After editing, start a new session and verify Claude follows the rules.
User Memory: Your Global Preferences
What Is User Memory?
The user-level CLAUDE.md file (~/.claude/CLAUDE.md) stores your personal coding preferences that apply across all projects. Unlike the project CLAUDE.md which is shared with your team, this file is local to your machine.
This is the right place for preferences like your preferred language style, editor configuration habits, and communication preferences with Claude.
Example: A User-Level CLAUDE.md
These preferences apply to every project you open with Claude Code. If a project CLAUDE.md says "use 4-space indentation" and your user CLAUDE.md says "2-space indentation," both instructions are present in context — the project-specific instruction typically takes priority because it is more contextually relevant.
Local Memory: Personal Project Overrides
When to Use CLAUDE.local.md
The CLAUDE.local.md file sits in your project root alongside CLAUDE.md, but it is not committed to version control. Use it for:
- Personal context that only matters to you (e.g., "I am working on the payments module this sprint")
- Override instructions that conflict with the shared CLAUDE.md
- Temporary notes or debugging context for your current task
- References to local environment specifics (e.g., local API URLs, database credentials path)
Dynamic Imports with @ Syntax
Pulling in External Context
Sometimes your instructions reference external documentation or shared conventions that live in separate files. Instead of copying that content into CLAUDE.md, you can use the @ import syntax to pull it in dynamically:
When Claude Code loads this CLAUDE.md, it reads the referenced files and injects their content into context. The @ prefix tells Claude Code to resolve the path relative to the file that contains the import.
How Imports Work
- Paths are relative to the file containing the
@directive. - Recursive imports are supported — an imported file can itself contain
@imports, up to a maximum depth of 5 hops. - Missing files are silently skipped — if a referenced file does not exist, Claude Code continues without error.
This feature is especially powerful for large teams. You can maintain a central docs/ directory with shared standards and import those standards into individual project CLAUDE.md files, keeping everything in sync without duplication.
Path-Specific Rules with .claude/rules/
Fine-Grained Instructions
For projects where different directories have different conventions (e.g., frontend vs. backend), you can place rules in the .claude/rules/ directory with YAML frontmatter that specifies which file paths they apply to:
These rules are loaded lazily — they are only injected into context when Claude is working on files that match the specified paths. This keeps the context lean and relevant: frontend rules do not clutter the context when Claude is editing backend code.
Auto Memory: Claude's Own Notes
How Auto Memory Works
In addition to the CLAUDE.md files you write, Claude Code maintains its own memory system. When you tell Claude something important during a conversation (e.g., "always use tabs in this project" or "the API key is stored in Vault"), Claude can save that as a memory note.
Auto memory files are stored at:
Claude can also create topic-specific memory files in the same directory. These files are loaded at the start of each session — the first 200 lines or 25 KB (whichever is smaller) are loaded automatically. Additional topic files are loaded on demand when they become relevant.
Managing Auto Memory
You can browse and manage auto memory using the /memory command:
This opens an interactive browser showing all loaded memory files — both your CLAUDE.md files and Claude's auto-generated notes. You can review, edit, or delete entries as needed.
You can also explicitly ask Claude to remember something:
Claude will save this to its auto memory, and the note will be present in future sessions.
Automating Context with Hooks
What Are Hooks?
Hooks are user-defined scripts that execute automatically at specific points in Claude Code's lifecycle. Unlike CLAUDE.md instructions which are advisory (Claude reads them but may not always follow them perfectly), hooks are deterministic — they always run, and their output is always injected into context.
Hooks can run at many lifecycle points: when a session starts, when you submit a prompt, before or after tool use, when the context is compacted, and more. The full list of hook events includes:
- SessionStart — When a session begins or resumes
- UserPromptSubmit — When you submit a prompt, before Claude processes it
- PreToolUse — Before a tool call executes (can block it)
- PostToolUse — After a tool call succeeds
- Stop — When Claude finishes responding
- Notification — When Claude Code sends a notification
The Context Switcher Pattern
One powerful use of hooks is automatically injecting different context based on what you are working on. Here is a bash script that checks your current git branch and appends relevant context to your project's CLAUDE.md:
Let us break down how this script works:
- It reads the current git branch name using
git symbolic-ref. - The
add_context()helper function usesgrep -qxFto check if a line already exists in CLAUDE.md before appending it. The-qflag suppresses output,-xmatches the whole line, and-Ftreats the pattern as a fixed string. This makes the script idempotent — running it multiple times produces the same result. - The
casestatement matches the branch pattern and adds relevant documentation imports: - feature/* branches get testing and API convention docs
- bugfix/* branches get debugging and logging docs
- release/* branches get deployment and changelog docs
Wiring the Script with a Hook
To make this script run automatically every time you submit a prompt, configure a UserPromptSubmit hook. Add this to your project's .claude/settings.json:
With this configuration:
- Every time you submit a prompt, the
context-switcher.shscript runs automatically. - The script checks your branch and ensures the right documentation references are in CLAUDE.md.
- Claude then reads the updated CLAUDE.md (with the new
@imports) and has the relevant context for your current task.
Other Useful Hook Patterns
Beyond context switching, hooks enable many powerful automations:
Auto-Format Code After Edits
Run Prettier on every file Claude edits to maintain consistent formatting:
The matcher field ensures this hook only fires when Claude uses the Edit or Write tools — not after every tool call.
Re-Inject Context After Compaction
When Claude's context window fills up, it triggers compaction — a process that summarizes the conversation to free space. This can lose important details. Use a SessionStart hook with a compact matcher to re-inject critical reminders:
Any text your hook command writes to stdout is added to Claude's context. You can replace the echo with any command that produces dynamic output, like git log --oneline -5 to show recent commits.
Block Edits to Protected Files
Prevent Claude from modifying sensitive files like .env or package-lock.json:
Register this as a PreToolUse hook with the Edit|Write matcher. When Claude tries to edit a protected file, the hook exits with code 2, which blocks the action. The message written to stderr is fed back to Claude as feedback so it can adjust its approach.
How Hook Communication Works
Understanding the communication protocol is essential for writing effective hooks:
- Input: Claude Code passes event-specific JSON data to your script's stdin. For example, a
PreToolUsehook receives the tool name and its arguments. - Output: Your script communicates back through exit codes and stdout/stderr.
- Exit 0: The action proceeds. For
UserPromptSubmitandSessionStarthooks, anything written to stdout is added to Claude's context. - Exit 2: The action is blocked. The text written to stderr becomes feedback for Claude.
- Any other exit code: The action proceeds, but an error notice appears in the transcript.
Where to Configure Hooks
Hooks can be placed at different scopes depending on who should use them:
| Location | Scope | Shared? |
|---|---|---|
~/.claude/settings.json | All your projects | No, local to your machine |
.claude/settings.json | Single project | Yes, can be committed to the repo |
.claude/settings.local.json | Single project | No, gitignored |
Putting It All Together
A Complete Memory Setup
Here is how all the pieces fit together for a well-configured project:
With this setup, Claude Code automatically receives:
- Your personal coding preferences from
~/.claude/CLAUDE.md - Project conventions from
./CLAUDE.md - Your personal project notes from
./CLAUDE.local.md - Path-specific rules when working on frontend or backend files
- Dynamically imported documentation based on your current git branch
- Auto memory notes from previous sessions
The Memory Lifecycle
Understanding when each type of context is loaded helps you design an effective setup:
- Session starts: Claude Code loads all CLAUDE.md files (walking up the directory tree), plus the first 200 lines of auto memory.
- You submit a prompt:
UserPromptSubmithooks fire, potentially modifying CLAUDE.md or injecting additional context. - Claude works on files: Path-specific rules from
.claude/rules/are lazily loaded as Claude touches matching files. - Compaction occurs:
SessionStarthooks with thecompactmatcher fire, re-injecting critical context that might have been summarized away. - Claude learns something: If you tell Claude to remember a fact, it writes to auto memory for future sessions.
Best Practices
Do's
- Keep CLAUDE.md under 200 lines — Use
@imports for detailed documentation instead of inlining everything. - Commit project CLAUDE.md to git — Treat it as living documentation that evolves with your codebase.
- Use specific, actionable instructions — Write "Run
npm testbefore committing" not "Make sure tests pass." - Structure with Markdown headers and bullets — Claude parses structured content more effectively.
- Prune regularly — Remove outdated rules. Stale instructions are worse than no instructions.
- Use path-specific rules for large monorepos — Keep frontend and backend conventions separate.
- Make hook scripts idempotent — Scripts may run multiple times; always check before appending.
Don'ts
- Do not dump entire documentation into CLAUDE.md — Link with
@imports instead. - Do not store secrets in CLAUDE.md — These files may be committed to git.
- Do not write vague rules — "Write good code" tells Claude nothing useful.
- Do not skip testing your instructions — After editing CLAUDE.md, start a new session and verify Claude follows the rules.
- Do not ignore auto memory — Periodically review
/memoryand clean up notes that are no longer relevant.
Summary
Writing context is the most fundamental strategy for context engineering with Claude Code. The memory hierarchy — project CLAUDE.md, user CLAUDE.md, local overrides, path-specific rules, dynamic imports, and auto memory — gives you precise control over what Claude knows at every point in a session. Combined with hooks for automation, you can build a system where Claude consistently receives the right context for the right task, without manual effort.
In the next article, we will explore Strategy 2: Selecting and Focusing Context — techniques for dynamically choosing which parts of your codebase and documentation to load into Claude's context window based on the current task.