Skip to content

Memory System

Claude Code’s memory system gives the agent persistent, file-based knowledge that survives across sessions. Unlike the ephemeral conversation context (which is lost after compaction or session end), memory files accumulate learnings, preferences, and project knowledge over time.

graph TD
subgraph "Memory Directory (~/.claude/projects/<project>/memory/)"
E["MEMORY.md<br/>(index file, max 200 lines)"]
T1["topic-1.md"]
T2["topic-2.md"]
L["logs/YYYY/MM/YYYY-MM-DD.md<br/>(daily logs)"]
end
subgraph "System Prompt"
SP[Memory Prompt Section]
end
subgraph "Team Memory"
TM["Team member shared memory"]
end
E --> SP
T1 --> SP
TM --> SP

The memory directory follows a standard structure:

  • MEMORY.md — the index/entrypoint file, loaded into every system prompt
  • Topic files — detailed memory on specific topics, referenced from MEMORY.md
  • Daily logs — chronological activity logs (used in KAIROS/assistant mode)

Memory paths are resolved by getAutoMemPath() in src/memdir/paths.ts with a priority chain:

src/memdir/paths.ts
export const getAutoMemPath = memoize((): string => {
// 1. Env var override (Cowork/SDK)
const override = getAutoMemPathOverride() ?? getAutoMemPathSetting()
if (override) return override
// 2. Default: ~/.claude/projects/<sanitized-git-root>/memory/
const projectsDir = join(getMemoryBaseDir(), 'projects')
return join(projectsDir, sanitizePath(getAutoMemBase()), AUTO_MEM_DIRNAME) + sep
})

The resolution order:

PrioritySourceUse Case
1CLAUDE_COWORK_MEMORY_PATH_OVERRIDE env varCowork space-scoped mount
2autoMemoryDirectory in settings.jsonUser custom directory
3~/.claude/projects/<sanitized-path>/memory/Default

The base path uses findCanonicalGitRoot() so all worktrees of the same repository share one memory directory:

function getAutoMemBase(): string {
return findCanonicalGitRoot(getProjectRoot()) ?? getProjectRoot()
}

Auto-memory can be disabled through multiple mechanisms:

src/memdir/paths.ts
export function isAutoMemoryEnabled(): boolean {
// 1. Env var override (explicit off)
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY)) return false
// 2. Bare mode (--bare / SIMPLE)
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) return false
// 3. CCR without persistent storage
if (isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) &&
!process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR) return false
// 4. Settings.json opt-out
const settings = getInitialSettings()
if (settings.autoMemoryEnabled !== undefined) return settings.autoMemoryEnabled
// 5. Default: enabled
return true
}

MEMORY.md is the entrypoint that gets loaded into the system prompt. It has strict size limits:

src/memdir/memdir.ts
export const ENTRYPOINT_NAME = 'MEMORY.md'
export const MAX_ENTRYPOINT_LINES = 200
export const MAX_ENTRYPOINT_BYTES = 25_000 // ~125 chars/line at 200 lines

When MEMORY.md exceeds these limits, it’s truncated with a warning:

export function truncateEntrypointContent(raw: string): EntrypointTruncation {
const contentLines = raw.trim().split('\n')
const wasLineTruncated = contentLines.length > MAX_ENTRYPOINT_LINES
const wasByteTruncated = raw.trim().length > MAX_ENTRYPOINT_BYTES
if (!wasLineTruncated && !wasByteTruncated) {
return { content: raw.trim(), /* ... */ }
}
// Line-truncate first (natural boundary), then byte-truncate
let truncated = wasLineTruncated
? contentLines.slice(0, MAX_ENTRYPOINT_LINES).join('\n')
: raw.trim()
if (truncated.length > MAX_ENTRYPOINT_BYTES) {
const cutAt = truncated.lastIndexOf('\n', MAX_ENTRYPOINT_BYTES)
truncated = truncated.slice(0, cutAt > 0 ? cutAt : MAX_ENTRYPOINT_BYTES)
}
return {
content: truncated + `\n\n> WARNING: ${ENTRYPOINT_NAME} is ${reason}.
Only part of it was loaded. Keep index entries to one line under ~200 chars;
move detail into topic files.`,
// ...
}
}

The design forces MEMORY.md to be an index, not a dump. Each entry should be one line under ~200 chars, pointing to topic files for details.

Memory content is categorized by type, defined in src/memdir/memoryTypes.ts:

// Memory taxonomy (from memoryTypes.ts)
// - User preferences and style
// - Project architecture and conventions
// - Feedback and corrections
// - Reference documentation

The memory prompt includes specific guidance on what to save and what not to save:

import {
MEMORY_FRONTMATTER_EXAMPLE,
TRUSTING_RECALL_SECTION,
TYPES_SECTION_INDIVIDUAL,
WHAT_NOT_TO_SAVE_SECTION,
WHEN_TO_ACCESS_SECTION,
} from './memoryTypes.js'

Custom agents can declare their own memory scope:

// In agent frontmatter or JSON
memory: 'user' | 'project' | 'local'
ScopeLocationShared?
userUser-level directoryAcross all projects
projectProject-level directoryWithin project
localLocal-onlyNot shared

When an agent has memory enabled, its system prompt is dynamically appended:

src/tools/AgentTool/loadAgentsDir.ts
getSystemPrompt: () => {
if (isAutoMemoryEnabled() && memory) {
return systemPrompt + '\n\n' + loadAgentMemoryPrompt(agentType, memory)
}
return systemPrompt
}

Additionally, file tools (Read, Edit, Write) are automatically injected into the agent’s tool set when memory is enabled:

if (isAutoMemoryEnabled() && parsed.memory && tools !== undefined) {
const toolSet = new Set(tools)
for (const tool of [FILE_WRITE_TOOL_NAME, FILE_EDIT_TOOL_NAME, FILE_READ_TOOL_NAME]) {
if (!toolSet.has(tool)) {
tools = [...tools, tool]
}
}
}

The AGENT_MEMORY_SNAPSHOT feature enables project-level memory snapshots — a way to distribute pre-built memory to all users of a project:

src/tools/AgentTool/agentMemorySnapshot.ts
async function initializeAgentMemorySnapshots(agents: CustomAgentDefinition[]): Promise<void> {
await Promise.all(agents.map(async agent => {
if (agent.memory !== 'user') return
const result = await checkAgentMemorySnapshot(agent.agentType, agent.memory)
switch (result.action) {
case 'initialize':
// Copy snapshot to local if no local memory exists
await initializeFromSnapshot(agent.agentType, agent.memory, result.snapshotTimestamp!)
break
case 'prompt-update':
// Newer snapshot available — flag for user prompt
agent.pendingSnapshotUpdate = { snapshotTimestamp: result.snapshotTimestamp! }
break
}
}))
}

The TEAMMEM feature enables shared memory across team members:

// src/memdir/memdir.ts — conditional import
const teamMemPaths = feature('TEAMMEM')
? require('./teamMemPaths.js')
: null

Team memory paths (src/memdir/teamMemPaths.ts) and team memory prompts (src/memdir/teamMemPrompts.ts) allow teammates to share a common memory directory, enabling knowledge transfer between agents in the same team.

The EXTRACT_MEMORIES feature runs a background agent that extracts memories from the conversation:

src/memdir/paths.ts
export function isExtractModeActive(): boolean {
if (!getFeatureValue_CACHED_MAY_BE_STALE('tengu_passport_quail', false)) return false
return !getIsNonInteractiveSession() ||
getFeatureValue_CACHED_MAY_BE_STALE('tengu_slate_thimble', false)
}

The background extraction agent scans conversation turns for memorable information and writes it to the memory directory. When the main agent writes memories itself, the background agent skips those ranges (deduplication via hasMemoryWritesSince).

In KAIROS/assistant sessions, memory takes a different form — daily log files:

src/memdir/paths.ts
export function getAutoMemDailyLogPath(date: Date = new Date()): string {
const yyyy = date.getFullYear().toString()
const mm = (date.getMonth() + 1).toString().padStart(2, '0')
const dd = date.getDate().toString().padStart(2, '0')
return join(getAutoMemPath(), 'logs', yyyy, mm, `${yyyy}-${mm}-${dd}.md`)
}

Rather than maintaining MEMORY.md as a live index, the agent appends to date-named log files as it works. A separate nightly /dream skill distills these logs into topic files and MEMORY.md.

Memory paths undergo strict security validation:

// src/memdir/paths.ts — validateMemoryPath
function validateMemoryPath(raw: string | undefined, expandTilde: boolean): string | undefined {
// Reject: relative paths, root/near-root, Windows drive-root,
// UNC paths, null bytes
if (
!isAbsolute(normalized) ||
normalized.length < 3 ||
/^[A-Za-z]:$/.test(normalized) ||
normalized.startsWith('\\\\') ||
normalized.startsWith('//') ||
normalized.includes('\0')
) {
return undefined
}
return (normalized + sep).normalize('NFC')
}

The isAutoMemPath() function is used by the filesystem permission system to grant write access to memory files without explicit user permission — a carve-out that makes memory writes seamless but requires careful path validation to prevent abuse.