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

Three-Tier Memory · MEMORY.md Index Pattern Top: three tiers with separated responsibilities. Bottom: auto memory's always-loaded index + on-demand files. TIER COMPARISON Project Memory <repo>/CLAUDE.md Writer project developer (git-tracked) Scope per-project, stable across users Authority OVERRIDES defaults EXAMPLE "Use pnpm, not npm" "NEVER commit to main" User Memory ~/.claude/CLAUDE.md Writer user (hand-written) Scope cross-project, per user Authority lower than project EXAMPLE "Prefer type over interface" "Commit in imperative mood" Auto Memory ~/.claude/projects/<p>/memory/ Writer Claude (self-maintained) Scope per-project, dynamic Types user / feedback / project / reference EXAMPLE "Prefers terse responses" "Pipeline bugs in Linear INGEST" zoom in AUTO MEMORY · INDEX + ON-DEMAND PATTERN ALWAYS IN SYSTEM PROMPT MEMORY.md (index) ≤ 200 lines — one line per memory, always loaded - [Terse responses](feedback_terse.md) — user dislikes trailing summaries - [User role](user_role.md) — 10-yr Go dev, new to React - [Merge freeze](project_freeze.md) — 2026-03-05 mobile release - [Linear project](reference_linear.md) — pipeline bugs in INGEST · · · (each ≤ 150 chars) on demand LOADED ON DEMAND (full content) feedback_terse.md full markdown · why + how-to-apply user_role.md full markdown project_freeze.md full markdown · why + how-to-apply reference_linear.md full markdown Progressive disclosure: index is the hot path; full content only when relevant

Many people treat “memory” as a single store. In Claude Code’s source it’s two orthogonal taxonomies layered together:

TaxonomySource locationAxis
Memory File Typeutils/memory/types.ts6 types — by “who wrote it, where is the file
Memory Content Typememdir/memoryTypes.ts4 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:

TypeDescription suffixWho writesScope
User"(user's private global instructions for all projects)"User, hand-writtenCross-project, per user
Project"(project instructions, checked into the codebase)"Project developerPer project, cross-user
Local"(user's private project instructions, not checked in)"User, .gitignoredPer project, per user
Managed(no special suffix — policy layer)Enterprise / org adminMachine / org-level
AutoMem"(user's auto-memory, persists across conversations)"Claude, self-maintainedPer 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):

TypeWhat it storesWhen to saveWhen to use
userUser’s role, goals, responsibilities, knowledgeLearning user’s preferences / backgroundWork should be informed by user perspective
feedbackCorrections or confirmations of approachesUser corrects you, or confirms a non-obvious approachAvoid the same correction twice
projectWho’s doing what, why, whenLearning ongoing work / constraints / deadlinesUnderstanding request motivation
referencePointers to external systems (Linear / Grafana / Slack)User mentions external resources + their purposeUser 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 opus
  • isolation: 'worktree': runs in a temporary git worktree — subagent’s changes don’t pollute the main repo; review then merge after completion
  • isolation: 'remote' (ant-only): runs in a remote CCR environment — fully isolated filesystem / network
  • run_in_background: runs in background, doesn’t block main agent
  • mode: 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:

  1. Default (non-fork): subagent starts a fresh conversation with its own system prompt, can’t see parent conversation history. This is the “context firewall
  2. 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 log is 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

  1. Memory is two taxonomies layered: memory file type (who wrote, where, scope) + memory content type (what’s stored). Merging them leaves special needs homeless
  2. User / Project / Local are hard layers. “Does it go in git” is a hard line; different scenarios need separate stores
  3. Managed / TeamMem are essential for enterprise — policy-enforced rules vs team-synced shared rules; personal preferences can’t substitute
  4. MEMORY.md index needs dual limits: lines + bytes. p100 data shows why single-cap is insufficient
  5. Strong content-type classification (user / feedback / project / reference) lets memory age by type
  6. Project-typed memories save absolute dates — relative dates (“last week”, “next Thursday”) drift inside memory
  7. Memory’s project key is the canonical git root, not the path — necessary for multi-worktree / symlink scenarios
  8. AutoMem enable chain must be detailed: env var / CLI flag / settings.json / default — 5 layers of opt-out is a must for production
  9. Subagents are dual-isolation (execution + learning): default-isolated context + own scope-based persistent memory
  10. Verify before citing memory — make “grep first” a hard rule of memory use, or memory becomes a hallucination source
  11. Explicit exclusion list: What NOT to save and What to save are equally important; LLMs bloat without it
  12. 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
Was this page helpful?