Claude Code Memory System — Implementation Details
From system prompt injection to background auto-extraction, dissecting every technical detail of the memory system.
Architecture · Path Resolution · Prompt Injection · Auto-Extraction · Intelligent Retrieval · Memory Scanning · Agent Memory · Team Sync · Constants · Data Flow

1. Overall Architecture
The memory system is powered by 5 core modules working in concert:
| Module | Source Location | Responsibility |
|---|---|---|
| Path Resolution | src/memdir/paths.ts | Computes memory storage directory, handles overrides and security validation |
| Prompt Construction | src/memdir/memdir.ts | Injects memory instructions into the system prompt |
| Memory Scanning | src/memdir/memoryScan.ts | Scans directories, parses frontmatter, sorts entries |
| Intelligent Retrieval | src/memdir/findRelevantMemories.ts | Uses Sonnet to select memories relevant to the current query |
| Auto-Extraction | src/services/extractMemories/ | Background forked agent that extracts memories from conversations |
Auxiliary modules:
| Module | Source Location | Responsibility |
|---|---|---|
| AutoDream | src/services/autoDream/ | Background memory consolidation ("dreaming"); see 03-autodream.md |
| Type Definitions | src/memdir/memoryTypes.ts | Taxonomy and prompt templates for the four memory types |
| Freshness | src/memdir/memoryAge.ts | Calculates memory age, generates stale warnings |
| File Detection | src/utils/memoryFileDetection.ts | Determines whether a path belongs to the memory system |
| Agent Memory | src/tools/AgentTool/agentMemory.ts | Three-level memory directories exclusive to sub-agents |
| Team Sync | src/services/teamMemorySync/ | Remote upload/download of memories |
2. Path Resolution System

Core Function: getAutoMemPath()
Path resolution priority (highest to lowest):
1. CLAUDE_COWORK_MEMORY_PATH_OVERRIDE <- Cowork environment variable (full path)
2. settings.json -> autoMemoryDirectory <- User setting (supports ~/ expansion)
3. {memoryBase}/projects/{sanitized-git-root}/memory/ <- Default computed pathKey source src/memdir/paths.ts:223:
export const getAutoMemPath = memoize(
(): string => {
const override = getAutoMemPathOverride() ?? getAutoMemPathSetting()
if (override) return override
const projectsDir = join(getMemoryBaseDir(), 'projects')
return join(projectsDir, sanitizePath(getAutoMemBase()), AUTO_MEM_DIRNAME) + sep
},
() => getProjectRoot(), // Cache key = project root
)Path Security Validation
validateMemoryPath() rejects dangerous paths:
| Rejected Path | Reason |
|---|---|
../foo | Relative path, CWD-dependent |
/ or /a | Root path or too-short path |
C:\ | Windows drive root |
\\server\share | UNC network path |
Contains \0 | Null byte, can truncate in system calls |
Security restriction: Project-level .claude/settings.json is not allowed to set autoMemoryDirectory, preventing malicious repositories from gaining write access to sensitive directories like ~/.ssh.
Enable Conditions
The isAutoMemoryEnabled() check chain:
CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 -> Disabled
CLAUDE_CODE_SIMPLE (--bare) -> Disabled
Remote mode without REMOTE_MEMORY_DIR -> Disabled
settings.json autoMemoryEnabled -> Follows setting
Default -> Enabled3. System Prompt Injection

Entry Point: loadMemoryPrompt()
This is the interface between the memory system and the system prompt. It is called once at startup (cached via systemPromptSection).
loadMemoryPrompt()
|-- KAIROS mode? -> buildAssistantDailyLogPrompt() [log append mode]
|-- TEAMMEM enabled? -> buildCombinedMemoryPrompt() [personal + team dual directory]
|-- Normal mode -> buildMemoryLines() [single directory]
|-- Disabled -> return nullPrompt Structure Built by buildMemoryLines()
# auto memory
You have a persistent file-based memory system located at `{memoryDir}`...
## Types of memory <- Definitions and examples for all four types
## What NOT to save <- Exclusion rules
## How to save memories <- Two-step saving process
## When to access memories <- When to consult
## Before recommending <- Verify before citing
## Memory and other forms <- Distinction from Plan/Task
## MEMORY.md <- Index content (or "currently empty")MEMORY.md Truncation Strategy
truncateEntrypointContent() applies dual limits:
// First truncate by line count
if (lineCount > 200) -> Truncate to 200 lines
// Then truncate by byte size (handles extra-long lines)
if (bytes > 25,000) -> Truncate at last newline character
// Append warning
"WARNING: MEMORY.md is {reason}. Only part of it was loaded."Automatic Directory Creation
ensureMemoryDirExists() ensures the directory exists when loading the prompt:
- Recursively creates with
mkdir(handles the entire parent chain) - Swallows
EEXIST(idempotent) - Genuine permission errors are only logged, not thrown (the Write tool will surface the real error)
The prompt explicitly tells the model the directory already exists, avoiding wasted turns on ls or mkdir.
4. Automatic Memory Extraction

Trigger Timing
Triggered in handleStopHooks when the model produces a final response (no tool calls).
Key source: src/services/extractMemories/extractMemories.ts
Complete Extraction Flow
1. Model finishes response (no tool_use)
|
2. executeExtractMemories() is called
|
3. Guard checks:
- Is this the main agent? (sub-agents don't extract)
- Feature gate enabled?
- Auto-memory enabled?
- Not in remote mode?
- No parallel extraction in progress?
|
4. Frequency control:
turnsSinceLastExtraction++
if < tengu_bramble_lintel -> skip
|
5. Mutual exclusion check:
Main agent wrote memory itself? -> skip, advance cursor
|
6. Scan existing memory directory (scanMemoryFiles)
Generate manifest (formatMemoryManifest)
|
7. Build extraction prompt (buildExtractAutoOnlyPrompt)
|
8. Run forked agent (runForkedAgent)
- Shares parent session's prompt cache
- Max 5 turns
- Restricted tool permissions
|
9. Extract written file paths
Advance cursor to latest message
|
10. Notify user: "Memory updated in ..."Forked Agent
Auto-extraction uses runForkedAgent -- a perfect fork of the main session:
- Shared prompt cache: Avoids duplicate API cache creation costs
- Isolated execution: Does not affect the main session's message history
- Restricted tools: Only allows Read, Grep, Glob, read-only Bash, and Edit/Write within the memory directory
- No transcript recording: Prevents race conditions with the main thread
Tool Permissions (createAutoMemCanUseTool)
Allowed: Read, Grep, Glob (unrestricted)
Allowed: Bash (read-only commands only: ls, find, grep, cat, stat...)
Allowed: Edit/Write (only within auto-memory directory)
Denied: MCP, Agent, non-read-only Bash, other write operationsMutual Exclusion Mechanism
The main agent and the extraction agent are mutually exclusive:
function hasMemoryWritesSince(messages, sinceUuid): boolean {
// Scan all assistant messages after sinceUuid
// If any Edit/Write tool_use targets the auto-memory directory
// -> return true (skip extraction, advance cursor)
}This prevents duplicate saves: when the main agent has already written a memory, the background extraction is skipped.
Merge Mechanism
If a previous extraction is still running:
- The new context is queued (
pendingContext) - After the old extraction completes, a tail extraction is immediately launched
- The tail extraction only processes messages added between the two calls
5. Intelligent Memory Retrieval

How It Works
Each time the user sends a query, findRelevantMemories() is triggered:
1. scanMemoryFiles(memoryDir)
- Recursively reads all .md files (excludes MEMORY.md)
- Parses frontmatter (first 30 lines)
- Sorts by modification time in descending order
- Max 200 files
|
2. Filter out previously surfaced memories (alreadySurfaced)
|
3. Format manifest (formatMemoryManifest)
- [type] filename (ISO timestamp): description
|
4. Sonnet model selection (sideQuery)
- System prompt: You are a memory selector...
- User message: Query + Available memories + Recently used tools
- Output: JSON { selected_memories: string[] }
- Max 5 selections
|
5. Return selected memories as { path, mtimeMs }Sonnet Selector Prompt
You are selecting memories useful for Claude Code to handle the user's query.
You'll receive the user's query and a list of available memory files (with filenames and descriptions).
Return at most 5 memory filenames that are clearly useful.
- If uncertain whether something is useful, don't select it
- If nothing is clearly useful, return an empty list
- If a list of recently used tools is provided, don't select usage docs for those tools
(but DO select warnings/gotchas/known issues about those tools)Freshness Warning
Selected memories are injected into the context with freshness information:
function memoryFreshnessText(mtimeMs: number): string {
const d = memoryAgeDays(mtimeMs)
if (d <= 1) return '' // Today/yesterday: no warning
return `This memory is ${d} days old. Memories are point-in-time observations...
Verify against current code before asserting as fact.`
}6. Memory Scanning in Detail
scanMemoryFiles()
Key design: Single-pass (read-then-sort) to avoid double stat system calls.
async function scanMemoryFiles(memoryDir, signal): Promise<MemoryHeader[]> {
const entries = await readdir(memoryDir, { recursive: true })
const mdFiles = entries.filter(f => f.endsWith('.md') && basename(f) !== 'MEMORY.md')
// Read all files' frontmatter in parallel (first 30 lines)
const headerResults = await Promise.allSettled(
mdFiles.map(async (relativePath) => {
const { content, mtimeMs } = await readFileInRange(filePath, 0, 30)
const { frontmatter } = parseFrontmatter(content)
return { filename, filePath, mtimeMs, description, type }
})
)
// Filter successful results, sort by time descending, take first 200
return fulfilled.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, 200)
}formatMemoryManifest()
Generates a manifest consumed by Sonnet or the extraction agent:
- [feedback] testing_policy.md (2026-03-15T10:30:00.000Z): Integration tests use real DB
- [user] role.md (2026-03-14T08:00:00.000Z): Data scientist, focused on logging
- [project] freeze.md (2026-03-10T15:00:00.000Z): Merge freeze starting 3/57. Agent Memory

Sub-agents (launched via the Agent tool) have an independent three-level memory system:
| Scope | Path | Description |
|---|---|---|
| user | ~/.claude/agent-memory/{agentType}/ | Global user-level |
| project | .claude/agent-memory/{agentType}/ | Project-level (committed to VCS) |
| local | .claude/agent-memory-local/{agentType}/ | Local-level (not committed) |
Differences from main memory:
- No MEMORY.md index step (
skipIndex = true) - Files can be written directly without the two-step process
- Each agent type is isolated (explorer, planner, etc. each have their own directory)
8. Team Memory Sync
When the TEAMMEM feature flag is enabled:
Directory Structure
~/.claude/projects/{hash}/memory/
├── MEMORY.md <- Personal memory index
├── user_*.md <- Personal memories
└── team/ <- Team shared directory
├── MEMORY.md <- Team memory index
└── *.md <- Team memoriesSync API
GET /api/claude_code/team_memory?repo={owner/repo} <- Pull
PUT /api/claude_code/team_memory?repo={owner/repo} <- PushSync Semantics
- Pull: Server content overwrites local files
- Push: Only uploads keys with different content hashes (delta upload)
- Deletes don't propagate: Local deletions do not delete remote entries
- Limits: Single file max 250KB, upload body max 200KB (batched)
Team vs. Personal Routing Rules
In memoryTypes.ts, each type has a <scope> directive:
| Type | Default Scope |
|---|---|
| user | Always personal |
| feedback | Personal by default; project-level conventions go to team |
| project | Leans toward team |
| reference | Usually team |
9. Key Constants Quick Reference
// Index file
ENTRYPOINT_NAME = 'MEMORY.md'
MAX_ENTRYPOINT_LINES = 200
MAX_ENTRYPOINT_BYTES = 25_000
// Scanning
MAX_MEMORY_FILES = 200
FRONTMATTER_MAX_LINES = 30
// Path
AUTO_MEM_DIRNAME = 'memory'
// Extraction
maxTurns = 5 // Forked agent max 5 turns
// Retrieval
Max 5 relevant memories returned10. Data Flow Overview
┌─────────────────────────────────────────────────────┐
│ Session Startup │
│ │
│ loadMemoryPrompt() │
│ -> ensureMemoryDirExists() │
│ -> buildMemoryLines() + MEMORY.md content │
│ -> Inject into system prompt │
└────────────────────────┬────────────────────────────┘
|
┌─────────────────────────────────────────────────────┐
│ User Query │
│ │
│ findRelevantMemories() │
│ -> scanMemoryFiles() [scan + frontmatter] │
│ -> Sonnet selects up to 5 relevant memories │
│ -> Inject into conversation context │
│ + freshness warnings │
└────────────────────────┬────────────────────────────┘
|
┌─────────────────────────────────────────────────────┐
│ Claude's Response │
│ │
│ Model may write memories directly │
│ (following system prompt guidance) │
│ Or not -> triggers background extraction │
└────────────────────────┬────────────────────────────┘
|
┌─────────────────────────────────────────────────────┐
│ Background Auto-Extraction │
│ │
│ executeExtractMemories() │
│ -> Mutual exclusion check │
│ (main agent already wrote? skip) │
│ -> Build extraction prompt + memory manifest │
│ -> runForkedAgent() │
│ [shared cache, restricted tools, 5 turns] │
│ -> Write new memory files + update MEMORY.md │
│ -> Notify user │
└────────────────────────┬────────────────────────────┘
|
┌─────────────────────────────────────────────────────┐
│ Background Memory Consolidation (AutoDream) │
│ │
│ executeAutoDream() │
│ [triggers every 24h + 5 sessions] │
│ -> Five-gate check │
│ (toggle/time/throttle/session/lock) │
│ -> buildConsolidationPrompt() │
│ -> runForkedAgent() │
│ [read-only Bash, memory-dir writes only] │
│ -> Four phases: │
│ Orient -> Gather -> Consolidate -> Prune │
│ -> Merge duplicates / fix stale / compress index │
│ -> Notify user: "Improved N memories" │
│ │
│ See 03-autodream.md for details │
└─────────────────────────────────────────────────────┘