Memory Tool
The Memory tool lets Claude read and write files in a client-managed directory, giving agents persistent state across sessions without RAG infrastructure.
When to Use
- Agents that need to remember user preferences, project context, or past decisions
- Coding agents that accumulate knowledge about a codebase over sessions
- Support/assistant agents that build up a history per user
- Replacing "dump everything into system prompt" with structured persistent state
Core Concept
The Memory tool is a server-side tool (provided by Anthropic) that Claude can call to read, write, and list files in a directory you manage. The API gives the tool calls; your app executes them against real storage (local fs, S3, DB-backed fs).
const response = await client.beta.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 4096,
tools: [{ type: "memory_20250818", name: "memory" }],
messages: [...],
});
Implementing the Memory Backend
When Claude calls the memory tool, your app handles the file operations. Minimal implementation:
import fs from "fs/promises";
import path from "path";
const MEMORY_ROOT = "/var/app/memory";
async function handleMemoryCall(input: any, userId: string) {
const userRoot = path.join(MEMORY_ROOT, userId);
const safe = (p: string) => {
const resolved = path.resolve(userRoot, p.replace(/^\//, ""));
if (!resolved.startsWith(userRoot + path.sep)) throw new Error("Path escapes root");
return resolved;
};
switch (input.command) {
case "view": {
const target = safe(input.path);
const stat = await fs.stat(target).catch(() => null);
if (!stat) return { error: "not found" };
if (stat.isDirectory()) {
const entries = await fs.readdir(target);
return { content: entries.join("\n") };
}
return { content: await fs.readFile(target, "utf-8") };
}
case "create": {
await fs.mkdir(path.dirname(safe(input.path)), { recursive: true });
await fs.writeFile(safe(input.path), input.file_text);
return { content: "created" };
}
case "str_replace": {
const f = safe(input.path);
const text = await fs.readFile(f, "utf-8");
if (!text.includes(input.old_str)) return { error: "old_str not found" };
await fs.writeFile(f, text.replace(input.old_str, input.new_str));
return { content: "replaced" };
}
case "insert": {
const f = safe(input.path);
const lines = (await fs.readFile(f, "utf-8")).split("\n");
lines.splice(input.insert_line, 0, input.insert_text);
await fs.writeFile(f, lines.join("\n"));
return { content: "inserted" };
}
case "delete": {
await fs.rm(safe(input.path), { recursive: true, force: true });
return { content: "deleted" };
}
case "rename": {
await fs.rename(safe(input.old_path), safe(input.new_path));
return { content: "renamed" };
}
}
}
Per-user roots are critical — never share memory across users.
Tool-Use Loop
async function runWithMemory(userId: string, userMessage: string) {
const messages = [{ role: "user", content: userMessage }];
while (true) {
const response = await client.beta.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 4096,
tools: [{ type: "memory_20250818", name: "memory" }],
messages,
});
messages.push({ role: "assistant", content: response.content });
if (response.stop_reason !== "tool_use") return response;
const toolResults = [];
for (const block of response.content) {
if (block.type === "tool_use" && block.name === "memory") {
const result = await handleMemoryCall(block.input, userId);
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: result.content ?? result.error,
is_error: !!result.error,
});
}
}
messages.push({ role: "user", content: toolResults });
}
}
Suggested Directory Layout
Claude will structure the memory itself, but seeding a clean layout helps. Example for a coding agent:
/memory/
user_profile.md # who the user is, preferences
current_project.md # what we're working on
decisions/
2026-04-15-auth.md # architectural decisions
conventions.md # code style rules learned
blockers.md # open issues
Prompt the agent with this layout in the system prompt so it uses consistent paths.
System Prompt Pattern
You have persistent memory at /memory. Before answering, consult:
- /memory/user_profile.md
- /memory/current_project.md
After significant work, update memory:
- Save user preferences to /memory/user_profile.md
- Save decisions to /memory/decisions/YYYY-MM-DD-topic.md
- Keep /memory/current_project.md up to date with what we're doing
Use `view` to read, `create`/`str_replace` to write. Keep memory concise.
Memory vs Context vs RAG
| Approach | Best for | Downside |
|---|---|---|
| Memory tool | Structured state the agent curates itself | Agent has to remember to read/write |
| Large context | One-shot reasoning on full corpus | Expensive per call |
| RAG | Huge static corpus | Brittle retrieval; no synthesis across docs |
Memory shines when the agent decides what's worth remembering, not a retrieval system.
Versioning & Safety
- Store memory in a durable, versioned backend (git-backed fs works well)
- Snapshot before destructive operations (delete, rename)
- Audit-log every write
- Enforce size limits — agents can fill memory; cap per-user MBs
- Never execute anything from memory files (they can be indirectly user-controlled)
Anti-Patterns
- Global memory across users — huge privacy leak
- No size limit — agent writes 10GB over time
- No path validation — agent uses
../or absolute paths - Dumping conversation logs into memory verbatim — grows unbounded
- Forgetting to prompt the agent to consult memory — it won't know to look
Best Practices
- Per-user memory root, enforced with path sandboxing
- Seed a clean directory layout in the system prompt
- Cap memory size per user (e.g., 10 MB)
- Snapshot/backup before destructive ops
- Audit every operation
- Periodically prompt the agent to compress/clean its own memory