Persistent Memory for Claude Code: How Cross-Session Context Actually Works
You close your laptop on Friday after a three-hour session where Claude Code refactored your auth module, learned that your team uses a custom Result<T> wrapper instead of throwing exceptions, and finally understood why UserService can’t import from billing/. On Monday you open a fresh session and Claude confidently wraps a new function in a try/catch and imports the forbidden module. Everything it learned is gone.
This is the defining ergonomic problem of agentic coding tools: the model has no episodic memory. Every session starts from zero plus whatever you’ve manually written down. Let’s engineer our way out of it — honestly, including the parts that don’t work.
The core problem: sessions are stateless by design
A Claude Code session is a conversation. When it ends, the transcript is archived and the working context — the accumulated understanding built up over dozens of tool calls — evaporates. The next session reconstructs context from three sources:
- Your
CLAUDE.mdfiles (project and user scope) - Whatever files Claude reads during the session
- What you type
None of these capture derived knowledge: the dead ends you already explored, the reason a weird workaround exists, the fact that you tried approach A and it broke CI. Re-deriving that knowledge costs tokens and wall-clock time on every single session.
The instinct is to build a memory layer — capture what happened, compress it, and inject the relevant slice back at startup. The open-source claude-mem project is a clean reference implementation of exactly this pattern, and it’s worth studying whether or not you adopt it.
The architecture pattern behind claude code persistent memory
Every serious approach to claude code persistent memory follows the same three-stage loop. Understanding the stages independently lets you build, buy, or skip each one.
Stage 1: Capture
You need to observe what happened in a session. Claude Code exposes lifecycle hooks — shell commands that fire on events. The relevant ones:
| Hook | Fires when | Use for |
|---|---|---|
SessionStart | A session begins | Inject memory |
SessionEnd | A session terminates | Trigger capture |
PostToolUse | After any tool call | Stream granular activity |
Stop | The agent finishes responding | Checkpoint |
A minimal capture hook in .claude/settings.json:
{
"hooks": {
"SessionEnd": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/mem/capture.sh"
}
]
}
]
}
}
The hook receives JSON on stdin describing the event. For SessionEnd you get the session ID and transcript path, so capture.sh can read the full conversation log:
#!/usr/bin/env bash
INPUT=$(cat)
TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path')
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
# Hand the raw transcript to the compression stage
python3 "$CLAUDE_PROJECT_DIR/.claude/mem/compress.py" \
--transcript "$TRANSCRIPT" \
--session "$SESSION_ID"
A common gotcha: hooks run with a timeout, and a large transcript passed synchronously to an LLM will blow past it. In practice you want capture to be fire-and-forget — write the transcript path to a queue and process it in a background worker, not inline in the hook.
Stage 2: Compress
Raw transcripts are useless as memory — they’re enormous and full of noise (file dumps, tool spam, your typos). The compression stage uses an LLM to distill the session into durable facts. This is where you spend a Haiku 4.5 call, not an Opus one — summarization is exactly the kind of high-volume, low-stakes task Haiku is priced for.
import anthropic
client = anthropic.Anthropic()
PROMPT = """You are a memory compressor. From this coding session
transcript, extract ONLY durable facts worth remembering next session:
- Architectural decisions and their rationale
- Conventions discovered (naming, error handling, imports)
- Dead ends already explored (so we don't repeat them)
- Unresolved TODOs
Output JSON: a list of {fact, category, files[], confidence}.
Omit anything transient. If nothing durable happened, output []."""
resp = client.messages.create(
model="claude-haiku-4-5",
max_tokens=1500,
system=PROMPT,
messages=[{"role": "user", "content": transcript_text}],
)
memories = resp.content[0].text
Note the confidence field and the explicit instruction to output []. Both are damage control for the failure mode we’ll get to. Store the output somewhere queryable — SQLite with an embedding column, or a vector store keyed by project and file paths.
Stage 3: Inject
At session start, you retrieve the relevant memories and inject them. The SessionStart hook can print to stdout, and that output is added to Claude’s context:
{
"hooks": {
"SessionStart": [
{
"matcher": "*",
"hooks": [
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/mem/inject.sh" }
]
}
]
}
}
The critical engineering decision is relevance filtering. Injecting all memory is the naive move and it’s wrong — it balloons your context and drowns the useful facts. Better: retrieve by what the session is likely about. If you can’t tell yet, inject only high-confidence, project-wide facts and expose the rest through an MCP server so Claude can query memory on demand:
# A tiny MCP tool the agent calls when it wants history
@mcp.tool()
def recall(query: str, files: list[str] = []) -> list[dict]:
"""Retrieve relevant memories about this codebase."""
return memory_store.search(query, files=files, min_confidence=0.6)
This pull model beats the push model in practice. Instead of guessing what’s relevant at startup, you let the agent ask “have we touched this billing code before?” precisely when it matters. The hook seeds a small always-on summary; the MCP server handles targeted recall.
Where this breaks — and it does break
I’ve run memory layers long enough to be skeptical of them. The failure modes are real and they compound.
AI-compressed memory hallucinates. The compressor is itself a language model. It will occasionally record a “decision” that was actually a discarded idea, or invert the rationale for a choice. Now you have authoritative-sounding false memory injected at startup, and the agent trusts it more than it should. This is worse than no memory, because it’s confidently wrong. The confidence field and human review of the memory store are the only real mitigations.
Stale context is a silent killer. You refactor UserService on Tuesday. Your Monday memory still says “auth lives in UserService.” Nothing invalidates it. Memories decay in accuracy the moment the code changes, and there’s no clean signal to expire them. Tie memories to file hashes or commit SHAs and treat them as suspect when the underlying file moves.
Token costs rise on every session. Injected memory is context you pay for on every turn, whether or not it gets used. A memory layer that injects 4K tokens at startup adds that to your input cost for the entire session. The pull-based MCP model helps, but capture and compression are their own recurring spend.
Here’s the honest comparison:
| Approach | Setup cost | Recurring tokens | Staleness risk | Best for |
|---|---|---|---|---|
CLAUDE.md only | Minutes | Low, deterministic | You control it | Most projects |
| Scoped MCP tools | Hours | On-demand only | Low | Live system state |
| Full memory layer | Days | Higher, per-session | High | Long, exploratory work |
The uncomfortable truth: you probably don’t need it
Before you build any of this, write a good CLAUDE.md. A hand-curated file that says “we use Result<T>, never throw; UserService must not import billing/; auth was moved to auth/ in Q2” solves 80% of the memory problem with zero hallucination risk and deterministic cost. It’s human-authored, so it’s trustworthy by construction.
Memory layers earn their keep in a narrower band than the hype suggests: long-running, exploratory work where the derived knowledge (dead ends, half-finished investigations) genuinely can’t be pre-written because you don’t know it yet. For steady-state feature work, CLAUDE.md plus a couple of scoped MCP servers covers more ground than an automated memory pipeline, and it never lies to you.
If you do build the pipeline, the compression stage is where cost sensitivity matters most — running frequent Haiku summarization is cheap, but only if your per-token pricing is reasonable to begin with, which is one reason teams route this kind of high-volume background work through providers like AI Prime Tech.
Practical takeaways
- Sessions are stateless. Derived knowledge — dead ends, rationale, conventions — is what’s actually lost, and it’s the expensive thing to re-derive.
- The pattern is capture → compress → inject. Wire capture and injection with
SessionEnd/SessionStarthooks; do capture asynchronously to dodge hook timeouts. - Prefer pull over push. Seed a tiny summary at startup, then expose a
recallMCP tool so the agent fetches memory only when relevant. - Compress with Haiku 4.5, tag every memory with confidence and source file hashes, and treat memory as suspect when files change.
- Automated memory can lie. Hallucinated and stale context is worse than none because it’s authoritative. Keep a human in the loop on the memory store.
- Try
CLAUDE.mdfirst. For most projects a hand-written context file plus scoped tooling beats a full memory layer on cost, trust, and maintenance — reach for persistence only when your work is genuinely long and exploratory.
One API key for Claude Opus 4.8, Sonnet 4.6, Haiku 4.5, Fable 5, plus GPT & Gemini — up to 80% off official pricing, pay-as-you-go.
Get Your API Key →