Claude Code Memory Setup: Context That Persists
Why Claude Code forgets things (and how memory works)
Claude Code does not have persistent memory by default. Each session starts with a blank conversational slate. What Claude "knows" at the start of a session comes entirely from three sources: the system prompt built into Claude Code itself, any CLAUDE.md files present in the directory hierarchy, and whatever you tell it in the current conversation.
This is not a bug. It is a design decision that keeps sessions isolated and prevents decisions made in one context from silently contaminating another. But it creates a real problem for professional use: every session, you have to re-establish architectural decisions, coding conventions, project context, and ongoing task state. That re-establishment costs tokens, time, and accuracy. Claude working from a freshly-stated rule is less reliable than Claude working from a rule it absorbed at session start.
The solution is a layered memory architecture. Each layer serves a different purpose, has a different persistence model, and contains different types of information. This guide walks through all four layers and shows you exactly what to put in each one.
Layer 1: CLAUDE.md - the foundation
CLAUDE.md is a Markdown file at your project root (or any directory in your project hierarchy). Claude Code reads it automatically at the start of every session. It is always-present context: everything in CLAUDE.md is available from the first message of every session without you having to re-state it.
Think of CLAUDE.md as the contract between you and Claude about how this project works. It contains the things that are true across all sessions: technology choices, architectural patterns, file structure, hard rules about what Claude must and must not do.
A well-structured CLAUDE.md for a full-stack TypeScript project:
# Project: E-commerce API
## Stack
- Runtime: Node.js 20.x
- Language: TypeScript 5.x, strict mode always on
- Framework: Fastify 4.x (NOT Express)
- ORM: Drizzle (NOT Prisma, NOT raw SQL)
- Database: PostgreSQL 16 (local dev via Docker, production on Neon)
- Auth: Better Auth (NOT NextAuth, NOT Clerk)
- Email: Resend (NOT SendGrid, NOT Nodemailer)
- Payments: Stripe (webhooks via src/app/api/webhooks/stripe/route.ts)
## File structure
- src/routes/ - Fastify route handlers (one file per domain, e.g. users.ts, products.ts)
- src/services/ - Business logic, no direct DB access
- src/repositories/ - All Drizzle queries live here, no business logic
- src/lib/ - Singleton clients: db.ts, resend.ts, stripe.ts
- src/types/ - Shared TypeScript interfaces and enums
- src/middleware/ - Fastify plugins: auth.ts, rate-limit.ts, logger.ts
- src/emails/ - React Email components (*.tsx)
- drizzle/ - Schema, migrations, config
## Hard rules
- NEVER write SQL in routes or services, always use the repository layer
- NEVER hardcode secrets, always read from process.env via src/lib/config.ts
- NEVER throw raw errors, wrap in AppError from src/lib/errors.ts
- NEVER create a new Drizzle/Resend/Stripe client inline, use singletons from src/lib/
- ALWAYS include text: alongside html: in every Resend send call
- ALWAYS handle the { data, error } tuple from Resend and Stripe SDK calls
- TypeScript strict mode: no any, no non-null assertion (!), explicit return types on all functions
## Patterns (use these, do not invent alternatives)
Error handling: throw new AppError('RESOURCE_NOT_FOUND', 'Product not found', 404)
Async handlers: export const handler: RouteHandler = async (req, reply) => { ... }
DB access: const users = await userRepository.findByEmail(email) (always via repository)
Config: const apiKey = config.stripe.secretKey (from src/lib/config.ts, not process.env)
## Current phase
The project is in active development, pre-launch.
Completed: auth, products, orders CRUD
In progress: payments integration
Not started: search, reviews, admin panel
The "Current phase" section is the one part of CLAUDE.md that you update as the project progresses. Everything else should be stable architectural decisions that do not change between sessions.
CLAUDE.md placement and stacking
Claude Code reads CLAUDE.md files from multiple locations and stacks them. The reading order, from most general to most specific:
~/.claude/CLAUDE.md(global, applies to all projects)/path/to/project/CLAUDE.md(project root)/path/to/project/src/CLAUDE.md(subdirectory, if present)/path/to/project/src/services/CLAUDE.md(nested subdirectory, if present)
More specific CLAUDE.md files override more general ones for conflicting rules. This stacking is useful for monorepos where each package has different conventions:
monorepo/
CLAUDE.md <- Root: shared conventions, monorepo structure
packages/
web/
CLAUDE.md <- Web app: Next.js rules, React patterns
api/
CLAUDE.md <- API: Fastify rules, no JSX
ui/
CLAUDE.md <- Component library: strict accessibility rules
When Claude Code is working in packages/api/, it reads the root CLAUDE.md for shared conventions and the packages/api/CLAUDE.md for API-specific rules. Rules in packages/api/CLAUDE.md take precedence if they conflict with the root.
Global CLAUDE.md for developer-wide preferences
~/.claude/CLAUDE.md applies to every Claude Code session on your machine, regardless of project. This is the right place for your personal development preferences and universal constraints:
# Global developer preferences
## Code style (always)
- No semicolons in JavaScript/TypeScript
- Single quotes, not double quotes
- 2-space indentation
- Arrow functions for all callbacks
- Trailing commas in all multi-line structures
## Security rules (always)
- NEVER commit API keys, tokens, or passwords to any file
- NEVER log sensitive data (passwords, tokens, PII)
- Always validate user input before using it in a query
- Environment variables for all external service credentials
## Communication style
- When suggesting changes, explain why not just what
- Flag potential security issues before suggesting a workaround
- If I ask for a quick fix that has a better architectural solution, note both
options and let me choose
- Do not generate placeholder comments like "// TODO: add error handling"
implement the error handling
The global CLAUDE.md is particularly valuable for the "communication style" section. These preferences apply across every project and every language, so they belong in the global file rather than in per-project CLAUDE.md.
Layer 2: The .claude/ directory
The .claude/ directory at your project root holds session-state files that change frequently. Unlike CLAUDE.md, which contains stable architectural rules, the .claude/ directory contains the living state of the project: the ongoing task context, session notes, and per-agent knowledge.
Claude Code reads the .claude/ directory contents when asked, but does not automatically load them on session start (unlike CLAUDE.md). You reference these files explicitly, or include pointers to them in CLAUDE.md.
A useful .claude/ directory structure:
.claude/
memory.md <- Current project state (updated each session)
tasks.md <- Active task list with status
decisions.md <- Architectural decisions log (why, not just what)
agent-memory/ <- Per-agent persistent knowledge
content.md
research.md
memory.md: living project state
memory.md holds the current, active context for the project. It is updated at the end of each session and read at the start of the next. Think of it as the project's working memory: what is happening right now, what was just completed, what is blocked.
# Project Memory - Updated 2026-06-11
## Now
Building the Stripe payment integration. Currently working on the checkout session
creation endpoint at src/routes/payments.ts.
## Recently completed
- Stripe webhook handler at src/app/api/webhooks/stripe/route.ts (complete)
- Drizzle orders table with payment_status field (migrated)
- Stripe client singleton at src/lib/stripe.ts (complete)
## Blocked
- Email confirmation after payment: blocked on Resend domain verification
(DNS records added, awaiting propagation)
## Key decisions made this week
- Using Stripe Checkout (not Elements) for the initial payment flow
(simpler, handles SCA/3DS automatically)
- Payment status stored in orders.payment_status, not a separate payments table
(sufficient for current scale, can normalise later)
## Next tasks
1. Implement order confirmation email (unblock after DNS propagation)
2. Add payment retry logic for failed webhooks
3. Write integration tests for the full checkout flow
CLAUDE.md should include a pointer to this file:
## Session start (MANDATORY)
Read .claude/memory.md at the start of every session.
It contains the current project state. Do not proceed without reading it.
With this rule in CLAUDE.md, every session starts by reading the current project state automatically. The state is always accurate because it was written at the end of the last session.
decisions.md: the architectural decision log
Architectural decisions deserve their own file separate from memory.md because they are permanent, not current. A decision made in week 1 of the project is still relevant in week 40. decisions.md is append-only: you add new decisions, you never overwrite old ones.
# Architectural Decisions
## 2026-06-01: Fastify over Express
**Decision**: Use Fastify 4 as the HTTP framework.
**Reason**: Built-in TypeScript support, plugin system, 3x throughput over Express
in benchmarks at expected request volume. Express requires additional boilerplate
for typing request/response objects.
**Alternatives considered**: Express (rejected: poor TypeScript ergonomics),
Hono (considered: excellent TS support, but Fastify ecosystem is larger for our needs).
## 2026-06-05: Drizzle over Prisma
**Decision**: Use Drizzle ORM for all database access.
**Reason**: Schema-as-code with better TypeScript inference than Prisma. Migration
control is explicit. Bundle size is smaller (important for edge deployments).
**Migration note**: If we switch to edge in future, Drizzle's edge-compatible
driver works with Neon's serverless driver. Prisma requires a separate edge
adapter with known issues.
## 2026-06-10: Stripe Checkout over Elements
**Decision**: Use Stripe Checkout for the initial payment flow.
**Reason**: Handles SCA, 3DS, and regional payment methods automatically.
Elements would require custom handling for each. Will revisit Elements if
we need a fully custom checkout UX.
Including a pointer in CLAUDE.md:
## Architectural decisions
See .claude/decisions.md before suggesting any architectural change.
These decisions have been made deliberately; do not reverse them without
flagging the conflict explicitly.
This rule stops Claude from "helpfully" suggesting a migration to Prisma or Express because it saw them in a Stack Overflow article. Claude reads the decisions log and knows those alternatives were evaluated and rejected.
Layer 3: Session handoff files
Session handoff files bridge the gap between sessions. CLAUDE.md provides stable context. memory.md provides current project state. Handoff files provide session-specific context: exactly what was in progress when the previous session ended, which files were modified, and what the next action is.
A handoff file written at the end of a session:
# Session Handoff - 2026-06-11
## Completed this session
- Implemented Stripe Checkout session creation at POST /api/checkout
- Added order status webhook handler: handles payment_intent.succeeded,
payment_intent.payment_failed
- Updated Drizzle schema: orders.payment_intent_id column added (migration 0015 applied)
- Stripe client singleton: src/lib/stripe.ts (reads STRIPE_SECRET_KEY from config)
## Files modified
- src/routes/payments.ts (new: checkout session creation endpoint)
- src/app/api/webhooks/stripe/route.ts (new: webhook handler)
- src/lib/drizzle/schema.ts (modified: added payment_intent_id to orders)
- src/lib/drizzle/migrations/0015_add_payment_intent_id.sql (new)
- src/lib/stripe.ts (new: client singleton)
- .env.local (modified: added STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET)
## Current state
- Checkout endpoint: working, tested with Stripe test mode
- Webhook handler: deployed locally, tested with Stripe CLI webhook forwarding
- Staging migration: NOT YET APPLIED (run before QA testing)
## Blockers
- Order confirmation email is blocked: Resend DNS not yet propagated
(claudify.tech SPF and DKIM added to Cloudflare, check again tomorrow)
## Next actions (in order)
1. Apply migration 0015 to staging: pnpm drizzle-kit push --config drizzle.staging.config.ts
2. Test full checkout flow on staging with Stripe test card 4242 4242 4242 4242
3. Once DNS propagates: implement order confirmation email (template already scaffolded
at src/emails/OrderConfirmation.tsx)
4. Add retry logic for failed webhook deliveries
Store this in .claude/handoffs/ with a date-stamped filename:
.claude/handoffs/2026-06-11.md
Update CLAUDE.md to point to the latest handoff:
## Session continuity
- At session start, after reading memory.md, check .claude/handoffs/ for the
most recent handoff file and read it
- Before ending a session, write a handoff to .claude/handoffs/YYYY-MM-DD.md
using the template structure above
The handoff file restores the specific, actionable context that did not belong in memory.md (because memory.md describes the project state, not the session detail) and that CLAUDE.md cannot hold (because CLAUDE.md is stable architectural rules, not ephemeral task state).
Layer 4: Agent memory files
When using Claude Code with subagents (via the agents API or Claude Code's agent spawning), each agent benefits from its own persistent memory file. This is useful for long-running automation where an agent accumulates domain-specific knowledge over multiple runs.
An agent memory file for a research agent:
# Research Agent Memory
## Knowledge accumulated
### Stripe payment links
- Stripe Payment Links max out at 50 custom fields per link
- Stripe requires HTTPS for all webhooks in production (HTTP allowed in test mode)
- `payment_intent.succeeded` fires after card charge; `checkout.session.completed`
fires after user completes the checkout UI. Use `payment_intent.succeeded` for
revenue tracking, `checkout.session.completed` for UX events.
### Resend deliverability
- Gmail penalises HTML-only emails; always include text: field
- DMARC propagation typically takes 24-48 hours on Cloudflare
- Resend batch limit: 100 emails per API call
### Drizzle patterns
- Use drizzle-orm/pg-core for PostgreSQL column definitions
- Serial vs integer primary key: use serial() for auto-increment, not integer() + default()
- The .returning() clause on insert lets you get the inserted row back without a second query
## Mistakes to avoid (learned from previous runs)
- Do NOT use replyTo (camelCase) with raw Resend API; use reply_to (snake_case)
- Do NOT skip the { data, error } destructure on Resend/Stripe SDK calls
- Do NOT modify schema.ts directly for column additions; always generate a migration
Agent memory files grow over time as the agent learns. At the start of each agent run, the agent reads its memory file and integrates the accumulated knowledge into its approach. Mistakes learned in run 3 inform behaviour in run 30.
Store agent memory in .claude/agent-memory/:
.claude/agent-memory/
research.md
content.md
deployment.md
Putting the layers together
Each layer has a distinct role:
| Layer | What it holds | Update frequency | Who reads it |
|---|---|---|---|
| CLAUDE.md (global) | Developer-wide preferences | Rarely | All sessions |
| CLAUDE.md (project) | Project architecture, hard rules | Occasionally | All project sessions |
| .claude/memory.md | Current project state | Each session | At session start |
| .claude/decisions.md | Architectural decision history | When decisions are made | Before suggesting changes |
| .claude/handoffs/ | Session-specific task context | End of each session | At session start |
| .claude/agent-memory/ | Per-agent accumulated knowledge | After each agent run | Each agent run start |
A session with all four layers in place looks like this:
- Session starts: Claude Code reads CLAUDE.md automatically.
- First message: "Read .claude/memory.md and the latest .claude/handoffs/ file."
- Claude absorbs the current project state and the last session's handoff.
- Work proceeds with full context: architectural constraints from CLAUDE.md, current state from memory.md, specific task context from the handoff.
- Session ends: You (or Claude, if instructed) writes a new handoff to
.claude/handoffs/YYYY-MM-DD.mdand updates.claude/memory.md.
What NOT to put in CLAUDE.md
CLAUDE.md is valuable because it is compact and always-present. Keep it under 100 lines by being ruthless about what belongs there.
Do not put in CLAUDE.md:
- Specific file contents or code snippets (they belong in the actual source files)
- Detailed task histories (belongs in memory.md or handoffs)
- Full API reference documentation (belongs in a link to the actual docs)
- Implementation notes for specific past tasks (belongs in decisions.md if it is a decision, or handoffs if it is task context)
Put in CLAUDE.md:
- Technology choices (which framework, which ORM, which auth library)
- File structure overview (what lives where)
- Hard rules (always, never, mandatory patterns)
- Quick reference for frequently used patterns
- Session start instructions (read memory.md, read latest handoff)
A 60-line CLAUDE.md that perfectly captures the project's architecture is more valuable than a 400-line CLAUDE.md that includes everything. The longer CLAUDE.md consumes more tokens on every session and makes it harder to find the actually important rules when Claude needs them.
Checking that memory is working
After setting up the layers, verify that each one is working correctly with a simple test at the start of a fresh session:
New session. Without me telling you anything about the project,
what do you know about:
1. The project's technology stack?
2. The current task state?
3. The last session's completed work?
4. The next action?
Claude should be able to answer all four questions from CLAUDE.md (stack) and the memory/handoff files (current state, last session, next action). If any answer is "I don't know", that layer's information is missing or not being loaded correctly.
For a deeper treatment of how to manage context during a session so this memory stays effective, see Claude Code context management. For understanding the token cost of loading each memory layer and how to balance depth against cost, see Claude Code token usage.
Get Claudify. A complete CLAUDE.md template system with all four memory layers pre-configured, ready to drop into any TypeScript project and start with working context from session one.
More like this
Ready to upgrade your Claude Code setup?
Get Claudify