Claude Code Foundations of Context Engineering Created: 13 Apr 2026 Updated: 13 Apr 2026

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:

  1. 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.
  2. Auto memory — Notes that Claude creates and maintains on its own (or when you use the /memory command). 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:

TierFile LocationPurposeShared via Git?
Project Memory./CLAUDE.md (project root)Project-wide coding standards, commands, architectureYes
User Memory~/.claude/CLAUDE.mdPersonal preferences that apply to all your projectsNo
Local Memory./CLAUDE.local.md (project root)Personal overrides for a specific projectNo (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:

  1. ~/.claude/CLAUDE.md (user-level, always loaded)
  2. ~/projects/CLAUDE.md + ~/projects/CLAUDE.local.md
  3. ~/projects/myapp/CLAUDE.md + ~/projects/myapp/CLAUDE.local.md
  4. ~/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:

  1. Build and test commands — How to install dependencies, run tests, lint, and build
  2. Code style conventions — Formatting rules, naming conventions, import ordering
  3. Architecture overview — Key directories, important files, how modules connect
  4. Workflow rules — Branch naming, commit message format, PR process
  5. 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:

/init

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:

# Project: order-api

## Architecture
- Node.js + Express microservices
- PostgreSQL database with Knex migrations
- Redis for caching and session management
- Docker Compose for local development

## Commands
- Install: `npm install`
- Dev server: `npm run dev`
- Test all: `npm test`
- Test single: `npm test -- --grep "test name"`
- Lint: `npm run lint`
- Migrate DB: `npm run migrate`

## Code Style
- ESLint with Airbnb config
- Prettier with 2-space indent, single quotes
- Use async/await, never raw Promises
- Error handling: always use the AppError class from src/utils/errors.js

## Conventions
- Route handlers in src/routes/, one file per resource
- Business logic in src/services/, never in route handlers
- Database queries in src/repositories/
- All environment variables must be listed in .env.example

## Git Workflow
- Branch from main
- Branch naming: feature/TICKET-123-short-description
- Squash merge PRs
- Commit messages: "feat: description" or "fix: description"

## Do NOT
- Do not use var — use const or let
- Do not add console.log to production code — use the logger from src/utils/logger.js
- Do not modify migration files that have already been run

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:

  1. Be specific, not vague — Write Use vitest, not jest instead of Use the right testing framework.
  2. Be concise — Use short phrases and bullet points. Aim for under 200 lines total.
  3. Use Markdown headers and lists — Structure helps Claude parse the content quickly.
  4. Check it into version control — The project CLAUDE.md should be a shared team resource.
  5. Prune regularly — Remove rules that are no longer relevant. Stale instructions confuse the model.
  6. 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

# Personal Coding Preferences

## Language & Style
- TypeScript strict mode always
- 2-space indentation
- Prettier for formatting
- Prefer arrow functions over function declarations
- Use descriptive variable names, no abbreviations

## Communication
- Be concise in explanations
- Show code examples instead of long descriptions
- When making changes, explain what changed and why

## Testing
- Always suggest unit tests for new functions
- Use describe/it blocks with clear descriptions

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:

  1. Personal context that only matters to you (e.g., "I am working on the payments module this sprint")
  2. Override instructions that conflict with the shared CLAUDE.md
  3. Temporary notes or debugging context for your current task
  4. References to local environment specifics (e.g., local API URLs, database credentials path)
# Local Notes

## Current Focus
- Working on refactoring the auth middleware
- Related files: src/middleware/auth.js, src/services/authService.js

## My Local Setup
- Database running on port 5433 (not the default 5432)
- API keys in ~/.secrets/order-api.env

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:

# Project Instructions

Follow our API conventions:
@docs/api-conventions.md

Debugging tips and known issues:
@.claude/memory/debugging-notes.md

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

  1. Paths are relative to the file containing the @ directive.
  2. Recursive imports are supported — an imported file can itself contain @ imports, up to a maximum depth of 5 hops.
  3. 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:

# File: .claude/rules/frontend-rules.md
---
paths:
- "src/frontend/**"
- "*.tsx"
---

# Frontend Conventions
- Use React functional components only
- State management with Zustand
- All components must have PropTypes or TypeScript interfaces
# File: .claude/rules/backend-rules.md
---
paths:
- "src/api/**"
- "src/services/**"
---

# Backend Conventions
- Use Express middleware pattern
- All endpoints must have input validation with Joi
- Return standardized error responses using AppError

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/projects/<project-hash>/memory/MEMORY.md

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:

/memory

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:

Remember: in this project, we use pnpm instead of npm for all package management.

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:

  1. SessionStart — When a session begins or resumes
  2. UserPromptSubmit — When you submit a prompt, before Claude processes it
  3. PreToolUse — Before a tool call executes (can block it)
  4. PostToolUse — After a tool call succeeds
  5. Stop — When Claude finishes responding
  6. 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:

#!/bin/bash
# context-switcher.sh — Inject context based on git branch

BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
CLAUDE_FILE="./CLAUDE.md"

add_context() {
local line="$1"
grep -qxF "$line" "$CLAUDE_FILE" || echo "$line" >> "$CLAUDE_FILE"
}

case "$BRANCH" in
feature/*)
add_context "@docs/testing-strategy.md"
add_context "@docs/api-conventions.md"
;;
bugfix/*)
add_context "@docs/debugging-guide.md"
add_context "@docs/logging-conventions.md"
;;
release/*)
add_context "@docs/deployment-checklist.md"
add_context "@docs/changelog-format.md"
;;
esac

Let us break down how this script works:

  1. It reads the current git branch name using git symbolic-ref.
  2. The add_context() helper function uses grep -qxF to check if a line already exists in CLAUDE.md before appending it. The -q flag suppresses output, -x matches the whole line, and -F treats the pattern as a fixed string. This makes the script idempotent — running it multiple times produces the same result.
  3. The case statement matches the branch pattern and adds relevant documentation imports:
  4. feature/* branches get testing and API convention docs
  5. bugfix/* branches get debugging and logging docs
  6. 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:

{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "./context-switcher.sh"
}
]
}
]
}
}

With this configuration:

  1. Every time you submit a prompt, the context-switcher.sh script runs automatically.
  2. The script checks your branch and ensures the right documentation references are in CLAUDE.md.
  3. 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:

{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}

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:

{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'Reminder: use pnpm, not npm. Run pnpm test before committing. Current sprint: auth refactor.'"
}
]
}
]
}
}

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:

#!/bin/bash
# protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")

for pattern in "${PROTECTED_PATTERNS[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
exit 2
fi
done

exit 0

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:

  1. Input: Claude Code passes event-specific JSON data to your script's stdin. For example, a PreToolUse hook receives the tool name and its arguments.
  2. Output: Your script communicates back through exit codes and stdout/stderr.
  3. Exit 0: The action proceeds. For UserPromptSubmit and SessionStart hooks, anything written to stdout is added to Claude's context.
  4. Exit 2: The action is blocked. The text written to stderr becomes feedback for Claude.
  5. 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:

LocationScopeShared?
~/.claude/settings.jsonAll your projectsNo, local to your machine
.claude/settings.jsonSingle projectYes, can be committed to the repo
.claude/settings.local.jsonSingle projectNo, gitignored

Putting It All Together

A Complete Memory Setup

Here is how all the pieces fit together for a well-configured project:

~/.claude/
├── CLAUDE.md # Your global preferences (TypeScript strict, 2-space indent, etc.)
└── settings.json # Global hooks (notifications, personal automations)

~/projects/myapp/
├── CLAUDE.md # Project conventions (committed to git)
├── CLAUDE.local.md # Your personal project notes (gitignored)
├── .claude/
│ ├── settings.json # Project hooks (committed to git)
│ ├── settings.local.json # Personal hook overrides (gitignored)
│ └── rules/
│ ├── frontend-rules.md # Rules for frontend files only
│ └── backend-rules.md # Rules for backend files only
├── docs/
│ ├── api-conventions.md # Shared docs imported via @
│ ├── testing-strategy.md
│ └── debugging-guide.md
└── context-switcher.sh # Auto-context based on branch

With this setup, Claude Code automatically receives:

  1. Your personal coding preferences from ~/.claude/CLAUDE.md
  2. Project conventions from ./CLAUDE.md
  3. Your personal project notes from ./CLAUDE.local.md
  4. Path-specific rules when working on frontend or backend files
  5. Dynamically imported documentation based on your current git branch
  6. Auto memory notes from previous sessions

The Memory Lifecycle

Understanding when each type of context is loaded helps you design an effective setup:

  1. Session starts: Claude Code loads all CLAUDE.md files (walking up the directory tree), plus the first 200 lines of auto memory.
  2. You submit a prompt: UserPromptSubmit hooks fire, potentially modifying CLAUDE.md or injecting additional context.
  3. Claude works on files: Path-specific rules from .claude/rules/ are lazily loaded as Claude touches matching files.
  4. Compaction occurs: SessionStart hooks with the compact matcher fire, re-injecting critical context that might have been summarized away.
  5. Claude learns something: If you tell Claude to remember a fact, it writes to auto memory for future sessions.

Best Practices

Do's

  1. Keep CLAUDE.md under 200 lines — Use @ imports for detailed documentation instead of inlining everything.
  2. Commit project CLAUDE.md to git — Treat it as living documentation that evolves with your codebase.
  3. Use specific, actionable instructions — Write "Run npm test before committing" not "Make sure tests pass."
  4. Structure with Markdown headers and bullets — Claude parses structured content more effectively.
  5. Prune regularly — Remove outdated rules. Stale instructions are worse than no instructions.
  6. Use path-specific rules for large monorepos — Keep frontend and backend conventions separate.
  7. Make hook scripts idempotent — Scripts may run multiple times; always check before appending.

Don'ts

  1. Do not dump entire documentation into CLAUDE.md — Link with @ imports instead.
  2. Do not store secrets in CLAUDE.md — These files may be committed to git.
  3. Do not write vague rules — "Write good code" tells Claude nothing useful.
  4. Do not skip testing your instructions — After editing CLAUDE.md, start a new session and verify Claude follows the rules.
  5. Do not ignore auto memory — Periodically review /memory and 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.


Share this lesson: