Memory System
Agent system authors — Claude Code's memory in source is 6 file types × 4 content types × 3 agent-persistence scopes; unpacking each layer's responsibility and implementation.
Memory isn’t one layer — it’s two orthogonal taxonomies
Many people treat “memory” as a single store. In Claude Code’s source it’s two orthogonal taxonomies layered together:
| Taxonomy | Source location | Axis |
|---|---|---|
| Memory File Type | utils/memory/types.ts | 6 types — by “who wrote it, where is the file” |
| Memory Content Type | memdir/memoryTypes.ts | 4 types — by “what information it stores” (only for entries within AutoMem) |
Each taxonomy answers a different question; together they describe a memory’s full identity. Unpacking each below.
Taxonomy 1: Six memory file types
// utils/memory/types.ts
export const MEMORY_TYPE_VALUES = [
'User', // ~/.claude/CLAUDE.md — user-global preferences
'Project', // <repo>/CLAUDE.md — project rules, git-tracked
'Local', // CLAUDE.local.md — **private** project rules, not git-tracked
'Managed', // policy / enterprise-managed config
'AutoMem', // auto-memory, persists across conversations
...(feature('TEAMMEM') ? (['TeamMem'] as const) : []), // team-shared memory
] as const
Each type has an explicit description suffix in the system prompt (utils/claudemd.ts lines 1169-1177),
injected as meta-information for the model:
| Type | Description suffix | Who writes | Scope |
|---|---|---|---|
| User | "(user's private global instructions for all projects)" | User, hand-written | Cross-project, per user |
| Project | "(project instructions, checked into the codebase)" | Project developer | Per project, cross-user |
| Local | "(user's private project instructions, not checked in)" | User, .gitignored | Per project, per user |
| Managed | (no special suffix — policy layer) | Enterprise / org admin | Machine / org-level |
| AutoMem | "(user's auto-memory, persists across conversations)" | Claude, self-maintained | Per project |
| TeamMem | "(shared team memory, synced across the organization)" | Team-synced (feature-flagged TEAMMEM) | Org-level |
Why 6 types (not arbitrary):
- Who has write authority: User / Local / Project are human-written; AutoMem is Claude-written; Managed is IT; TeamMem is team-synced
- Whether it enters git: Project yes, Local no — hard line
- Visibility: Local invisible to teammates; Project visible to everyone; User / AutoMem visible to yourself
- Update frequency: User / Project slow; AutoMem updates per-turn
Takeaway for your own agent: memory’s “who writes it × scope” are orthogonal axes. Merging them into one layer means special needs have nowhere to go. “Enterprise mandatory rules” shouldn’t share a file with “user preferences.”
Load order
getMemoryFiles() (utils/claudemd.ts line 790+) load order:
1. Managed (policy-level, always first)
2. Managed .claude/rules/*.md
3. User (if userSettings is enabled)
4. User ~/.claude/rules/*.md
5. Walk root → CWD, checking at each level:
- CLAUDE.md (Project)
- .claude/CLAUDE.md (Project)
- .claude/rules/*.md (Project)
- CLAUDE.local.md (Local)
Note .claude/rules/*.md — an extra rules directory at each level (Managed / User / Project / Local). It’s
finer-grained than a single CLAUDE.md; you can split rules by topic (testing rules, security rules, style rules)
into separate files.
Nested worktree handling
The source has a special branch for worktrees nested inside the main repo (utils/claudemd.ts lines 868-884):
When running from a git worktree nested inside its main repo… Skip Project-type files from directories above the worktree but within the main repo — the worktree already has its own checkout.
Scenario: running Claude Code from a worktree nested in the main repo means walking upward hits the main repo root and loads CLAUDE.md twice. Source explicitly skips.
Issue: github.com/anthropics/claude-code/issues/29599 — a real bug fix, not hypothetical. This detail usually only matters to teams running multi-worktree workflows, but when it bites you, debugging is painful.
Per-file hard limit
// utils/claudemd.ts
export const MAX_MEMORY_CHARACTER_COUNT = 40000
Any single CLAUDE.md or memory file over 40k characters gets truncated. 40k chars ≈ 6-8k tokens — repos with
several years and 1000+ contributors easily hit this.
Memory-loading off switches
CLAUDE_CODE_DISABLE_CLAUDE_MDS // Hard off for all CLAUDE.md loading
CLAUDE_CODE_DISABLE_AUTO_MEMORY // Only off for auto memory (see below)
--bare / CLAUDE_CODE_SIMPLE // Skip auto-discovery, keep --add-dir
CLAUDE_CODE_REMOTE // Cloud resume, skip git status
autoMemoryEnabled (settings.json) // Project-level opt-out
Taxonomy 2: Four content types (within AutoMem)
Each entry in AutoMem has a type from a strict 4-element set:
// memdir/memoryTypes.ts
export const MEMORY_TYPES = ['user', 'feedback', 'project', 'reference'] as const
The source comment states the design intent plainly:
Memories are constrained to four types capturing context NOT derivable from the current project state. Code patterns, architecture, git history, and file structure are derivable (via grep/git/CLAUDE.md) and should NOT be saved as memories.
Core principle: save only what can’t be derived from the current project state. Code structure can be
grepped, git history is in git log, project conventions are in CLAUDE.md — none of these go in memory. Memory
stores only what’s not elsewhere, but useful later.
Semantics of each type (from the source’s TYPES_SECTION_COMBINED):
| Type | What it stores | When to save | When to use |
|---|---|---|---|
| user | User’s role, goals, responsibilities, knowledge | Learning user’s preferences / background | Work should be informed by user perspective |
| feedback | Corrections or confirmations of approaches | User corrects you, or confirms a non-obvious approach | Avoid the same correction twice |
| project | Who’s doing what, why, when | Learning ongoing work / constraints / deadlines | Understanding request motivation |
| reference | Pointers to external systems (Linear / Grafana / Slack) | User mentions external resources + their purpose | User references external systems |
Why strong typing, not free-form
Different types age at different rates — feedback nearly never expires (preferences are stable); project changes fast (tasks finish); reference is medium; user is stable. Strong typing lets Claude judge “is this still valid?” at read time.
One specific rule for project: “relative dates in user messages must be converted to absolute dates when saving
(e.g., ‘Thursday’ → ‘2026-03-05’)”. Relative dates drift after writing, so the source’s memory rules explicitly
require converting to absolute dates.
MEMORY.md index: source-level constants
AutoMem’s storage structure:
~/.claude/projects/<sanitized-git-root>/memory/
MEMORY.md # Index (always loaded)
feedback_tool_truncation.md
user_role.md
project_current_initiative.md
...
Index hard constraints (memdir/memdir.ts):
export const ENTRYPOINT_NAME = 'MEMORY.md'
export const MAX_ENTRYPOINT_LINES = 200
export const MAX_ENTRYPOINT_BYTES = 25_000
Both limits are enforced — whichever breaks first triggers truncation:
- 200 lines is the first line — one index entry per line means up to ~200 memory summaries
- 25000 bytes is the second, targeting the long-line failure mode
The source comment explains why two:
~125 chars/line at 200 lines. At p97 today; catches long-line indexes that slip past the line cap (p100 observed: 197KB under 200 lines).
p100 data: someone’s MEMORY.md was only 200 lines but 197KB — meaning almost 1000 chars per line. A line-only cap wouldn’t catch it, hence the byte cap. “125 chars/line” is p97, meaning most indexes are tight.
When truncated, an explicit warning message (visible to the model) is appended:
WARNING: MEMORY.md is {reason}. Only part of it was loaded. Keep index entries
to one line under ~200 chars; move detail into topic files.
Truncation does lines first, then bytes; the byte cap uses lastIndexOf('\n', MAX_ENTRYPOINT_BYTES) to cut at
the nearest newline, not mid-line.
Takeaway for your own agent: unbounded-growth indexes need multi-dimensional hard limits + user-visible truncation warnings. A single limit (lines OR bytes) misses cases. The warning must teach how to fix — “Move detail into topic files” is actionable.
Two AutoMem modes: Live Index vs. Daily Log (KAIROS)
In default mode, Claude maintains MEMORY.md as a live index — every memory save reads MEMORY.md, decides
whether to update or append.
Under the KAIROS feature flag (comment in memdir/paths.ts), the model changes entirely:
Rather than maintaining MEMORY.md as a live index, the agent appends to a date-named log file as it works. A separate nightly /dream skill distills these logs into topic files + MEMORY.md.
KAIROS file layout:
<autoMemPath>/logs/2026/04/2026-04-22.md # Today's append-only log
<autoMemPath>/MEMORY.md # Updated by /dream periodically
<autoMemPath>/{topic}.md # Topic files curated by /dream
Core idea: cheap writes (append-only), organization via background job. Autonomous agents running N hours/day shouldn’t spend latency maintaining an index inline.
This resembles journaling — write-ahead log + background compaction. Fits autonomous agents very well.
Canonical git root: multiple worktrees share memory
// memdir/paths.ts
function getAutoMemBase(): string {
return findCanonicalGitRoot(getProjectRoot()) ?? getProjectRoot()
}
findCanonicalGitRoot’s meaning: all worktrees of the same repo share one auto-memory directory.
Issue the source cites: anthropics/claude-code#24382. Without this, each worktree has its own memory, and what’s learned in worktree A isn’t available in worktree B.
Takeaway for your own agent: memory’s project key should be canonical. A git repo’s “identity” isn’t the path — it’s the canonical root (or remote URL). Get this right early, no data migration later.
AutoMem env var controls
Auto memory has its own enabled-chain, independent of CLAUDE.md (memdir/paths.ts lines 30-55):
Priority chain (first defined wins):
1. CLAUDE_CODE_DISABLE_AUTO_MEMORY env var (1/true → OFF, 0/false → ON)
2. CLAUDE_CODE_SIMPLE (--bare) → OFF
3. CCR without persistent storage → OFF (no CLAUDE_CODE_REMOTE_MEMORY_DIR)
4. autoMemoryEnabled in settings.json (supports project-level opt-out)
5. Default: enabled
5 layers of priority, each with its own opt-out. User, org, cloud, project-level can all opt out independently — the off paths are as detailed as the on paths.
Subagents: context firewalls (source view)
Agent tool’s full signature
Source tools/AgentTool/AgentTool.tsx lines 82-102:
{
description: z.string(), // 3-5 word task description
prompt: z.string(), // The actual task
subagent_type: z.string().optional(),
model: z.enum(['sonnet', 'opus', 'haiku']).optional(), // Model override
run_in_background: z.boolean().optional(),
// Multi-agent params:
name: z.string().optional(), // Addressable via SendMessage
team_name: z.string().optional(),
mode: permissionModeSchema().optional(), // Permission mode override
// Isolation:
isolation: z.enum(['worktree', 'remote']).optional(), // Worktree temp / remote CCR
cwd: z.string().optional(), // Working directory override
}
Much richer than “subagent just isolates execution” — it’s a complete sub-Claude configuration system:
model: subagent can use a cheaper model (e.g., haiku) while main stays on opusisolation: 'worktree': runs in a temporary git worktree — subagent’s changes don’t pollute the main repo; review then merge after completionisolation: 'remote'(ant-only): runs in a remote CCR environment — fully isolated filesystem / networkrun_in_background: runs in background, doesn’t block main agentmode: subagent can have a different permission mode (e.g., subagent in plan, main in default)name+ SendMessage: async communication primitive between multi-agents
Two subagent paths
Source AgentTool.tsx lines 622-633 shows two execution paths:
// Default path: fresh context, doesn't inherit parent conversation
override: isForkPath ? {
systemPrompt: forkParentSystemPrompt // Fork path: use parent system prompt
} : enhancedSystemPrompt ? {
systemPrompt: asSystemPrompt(enhancedSystemPrompt) // Default: subagent's own prompt
} : undefined,
// Fork path inherits parent conversation history:
forkContextMessages: isForkPath ? toolUseContext.messages : undefined,
Two paths:
- Default (non-fork): subagent starts a fresh conversation with its own system prompt, can’t see parent conversation history. This is the “context firewall”
- Fork path: subagent inherits all parent messages + parent system prompt — for scenarios requiring full context
Most daily Agent tool calls go through the default path; fork is a safety valve for special cases.
Agent-level persistent memory (AgentMemoryScope)
Subagents have their own persistent memory, independent of main-agent AutoMem
(tools/AgentTool/agentMemory.ts):
export type AgentMemoryScope = 'user' | 'project' | 'local'
// - 'user' → ~/.claude/agent-memory/<agentType>/
// - 'project' → .claude/agent-memory/<agentType>/
// - 'local' → .claude/agent-memory-local/<agentType>/
Each subagent type (e.g., code-reviewer, database-migrator) has its own 3-scope memory directory. Memory
doesn’t leak between agents — one subagent’s learned preferences don’t pollute another.
Takeaway for your own agent: subagents aren’t just “execution isolation” — they should also be “learning isolation”. Different subagents handle different domains; their accumulated experience belongs in separate silos.
Three hard rules for memory usage
The source’s memory guidance (injected into system prompt as part of MEMORY_INSTRUCTION) has three hard rules for Claude’s behavior when reading memory:
1. Trust-but-verify
A memory that names a specific function, file, or flag is a claim that it existed when the memory was written. It may have been renamed, removed, or never merged. Before recommending it [verify].
Memory is a snapshot in time, not current truth. Before citing a specific identifier, grep / read to verify.
2. Explicit exclusion list
What NOT to save:
- Code patterns, conventions, architecture (derivable from code)
- Git history, recent changes (
git logis authoritative)- Debugging solutions (the fix is in the code)
- Anything already in CLAUDE.md (no duplicates)
- Ephemeral conversation state (use TaskCreate instead)
Telling the agent “what not to save” is as important as telling it what to save — LLMs default to “remember everything,” and unexplicit exclusion leads to bloat.
3. Don’t use if told not to
When to access memories: When memories seem relevant, or the user references prior-conversation work. […] If the user says to ignore or not use memory: Do not apply remembered facts, cite, compare against, or mention memory content.
The user can explicitly say “ignore memory” — agent must respect that. This is user sovereignty.
Takeaways for building your own agent
- Memory is two taxonomies layered: memory file type (who wrote, where, scope) + memory content type (what’s stored). Merging them leaves special needs homeless
- User / Project / Local are hard layers. “Does it go in git” is a hard line; different scenarios need separate stores
- Managed / TeamMem are essential for enterprise — policy-enforced rules vs team-synced shared rules; personal preferences can’t substitute
- MEMORY.md index needs dual limits: lines + bytes. p100 data shows why single-cap is insufficient
- Strong content-type classification (user / feedback / project / reference) lets memory age by type
- Project-typed memories save absolute dates — relative dates (“last week”, “next Thursday”) drift inside memory
- Memory’s project key is the canonical git root, not the path — necessary for multi-worktree / symlink scenarios
- AutoMem enable chain must be detailed: env var / CLI flag / settings.json / default — 5 layers of opt-out is a must for production
- Subagents are dual-isolation (execution + learning): default-isolated context + own scope-based persistent memory
- Verify before citing memory — make “grep first” a hard rule of memory use, or memory becomes a hallucination source
- Explicit exclusion list:
What NOT to saveandWhat to saveare equally important; LLMs bloat without it - Two MEMORY modes (live index vs. daily log + nightly distill) suit different agent workflows — autonomous / long-running agents favor the latter
Further reading
- Claude Code source:
utils/memory/types.ts,utils/claudemd.ts,memdir/{memdir,paths,memoryTypes}.ts,tools/AgentTool/{AgentTool,agentMemory}.tsx - Memory Design — cross-product memory theory
- System Prompt Assembly — where the memory layer sits in the prompt
- Compaction — short-term conversation vs long-term memory layering