Claude Memory Tool + Context Editing: Agent Tutorial (2026)
July 14, 2026

A Claude agent forgets everything once a conversation ends, and long agentic runs choke on their own tool results. The Claude memory tool fixes the first problem with a file-based store Claude reads and writes itself; Context Editing fixes the second by clearing stale tool results mid-run12.
TL;DR
You'll wire up @anthropic-ai/sdk's client.beta.messages.toolRunner() with two first-party features: the Claude memory tool (memory_20250818, generally available, no beta header) so Claude can persist notes to a /memories directory that survives across sessions, and Context Editing (clear_tool_uses_20250919, in beta) so a long-running agent automatically clears old tool results instead of blowing its context window12. You'll run two separate Node processes to prove memory actually survives a fresh process, and you'll see the real error message you get if you try to read outside /memories. Runtime: Node 22+, @anthropic-ai/sdk@0.111.0. Build time: 25-30 minutes.
What you'll learn
- Which of the four things people call "Claude memory" this tutorial actually covers, and which three it deliberately doesn't
- How to give an agent a persistent
/memoriesdirectory with the ready-madeBetaLocalFilesystemMemoryTool, and how to write your own handler for custom storage - How to prove memory survives across sessions by running two independent Node processes against the same store
- How Context Editing's
clear_tool_uses_20250919strategy works, and the exact config knobs (trigger,keep,clear_at_least,exclude_tools) - The difference between Context Editing (client-visible, selective) and Compaction (server-side, replaces the whole history) — and a real deprecation the SDK's own types reveal that a lot of blog posts miss
- How to combine memory and context editing in one
toolRunner()call so Claude saves what matters before old tool results get cleared
Prerequisites
- Node.js 22 or later. Node 24 is the current Active LTS and Node 22 is in Maintenance LTS as of this writing3; this tutorial was built and executed against Node 22.22.3.
@anthropic-ai/sdk@0.111.0— the registry-confirmedlatestas of 2026-07-144. This package ships every few days, so pin the exact version rather than using^orlatestin production.- TypeScript 7.0.2 and
tsx@4.23.1(both registry-confirmed current versions), plus@types/node@26.1.14. - An
ANTHROPIC_API_KEYif you want to run the full agent loop against the live API. The memory-persistence demo in Step 2 needs no API key at all — it exercises the storage layer directly. Live calls bill at standard Messages API rates: theclaude-opus-4-8model used in this tutorial's examples runs $5/MTok input and $25/MTok output; swap inclaude-haiku-4-5($1/$5 per MTok) for cheaper experimentation while you're iterating5. See the Verification section for exactly what was and wasn't run against the live API.
Which "Claude memory" are we even talking about?
Anthropic ships at least four different things people call "Claude memory," and mixing them up is one of the most common mistakes in blog posts on this topic. This tutorial is about exactly one of them:
- The Messages API memory tool (
memory_20250818) — a client-side tool where Claude requests file operations against a/memoriesdirectory that you host. This is what this tutorial covers. - Claude Agent SDK's
SessionStore— a different package (@anthropic-ai/claude-agent-sdk) that mirrors a session's raw transcript (every prompt, tool call, and response, verbatim) to external storage so that same conversation can be resumed later — not a knowledge base Claude actively curates and rereads the way memory files are. Covered in the human-in-the-loop Agent SDK tutorial, not here. - Claude Code's
CLAUDE.md— project-level instructions for the Claude Code CLI. Not an API feature at all. - Managed Agents' hosted memory stores and "Dreaming" — Anthropic's hosted agent product self-curates memory server-side; you don't implement the storage layer yourself. See Claude Managed Agents: Dreaming, Outcomes, Orchestration for that product.
The package itself keeps these separate too: @anthropic-ai/sdk ships a helpers/beta/memory.ts module for the memory tool (#1) and a completely separate resources/beta/memory-stores.ts REST resource for Managed Agents' hosted stores (#4) — confirmed by inspecting the installed package directly rather than assuming from the docs6.
Step 1: Set up the project
mkdir claude-agent-memory && cd claude-agent-memory
npm init -y
npm install @anthropic-ai/sdk@0.111.0
npm install -D typescript@7.0.2 tsx@4.23.1 @types/node@26.1.1
npx tsc --init --target ES2022 --module NodeNext --moduleResolution NodeNext --strict
Add "type": "module" to package.json and "noUncheckedIndexedAccess": true to the generated tsconfig.json's compilerOptions. Both matter: "type": "module" is what lets tsc treat top-level await as valid instead of failing with TS1309: The current file is a CommonJS module and cannot use 'await' at the top level — I hit that error for real while re-checking this tutorial's own code samples and confirmed the fix by adding the field and re-running tsc --noEmit, which then passed clean. With both settings in place, every code sample in this tutorial type-checks with zero errors and skipLibCheck left at its default false6. That's worth calling out because it's not always true: a different Anthropic package (@anthropic-ai/claude-agent-sdk) needs skipLibCheck: true to compile at all, due to unrelated internal type errors in its shipped .d.ts7. The base @anthropic-ai/sdk you're using here has no such problem.
Step 2: Give Claude a persistent memory directory
The fastest way to get a working memory store is Anthropic's ready-made local-filesystem implementation, BetaLocalFilesystemMemoryTool. It lives at a dedicated subpath export and implements the view, create, str_replace, insert, delete, and rename commands Claude's memory tool can issue16:
// session1.ts — simulates the FIRST agent session.
import { BetaLocalFilesystemMemoryTool } from "@anthropic-ai/sdk/tools/memory/node";
async function main() {
const memory = await BetaLocalFilesystemMemoryTool.init("./memory-store");
// Claude's first move is always to check its memory directory.
const listing = await memory.view({ command: "view", path: "/memories" });
console.log("--- view /memories (session 1, before writing) ---");
console.log(listing);
// Claude "discovers" something worth persisting and writes it.
const result = await memory.create({
command: "create",
path: "/memories/project_notes.md",
file_text:
"# Project: invoice-service\n\n" +
"- Postgres pool max is 10 connections in staging.\n" +
"- Retry logic lives in src/retry.ts, NOT in the queue consumer.\n",
});
console.log("--- create /memories/project_notes.md ---");
console.log(result);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
Run it:
npx tsx session1.ts
Real output from this exact script, executed in a clean sandbox:
--- view /memories (session 1, before writing) ---
Here're the files and directories up to 2 levels deep in /memories, excluding hidden items and node_modules:
4K /memories
--- create /memories/project_notes.md ---
File created successfully at: /memories/project_notes.md
Note that viewing an empty /memories directory is not an error — it returns a listing header plus one line for the (empty) directory itself, matching Anthropic's documented behavior for a first-time view call1.
Step 3: Prove memory survives a fresh process
This is the part most "memory tool" write-ups skip: they show you the API shape but never actually prove persistence across a session boundary. Run a second, independent script — a fresh Node process, a fresh BetaLocalFilesystemMemoryTool instance — pointed at the same directory:
// session2.ts — a completely separate process, no shared state with session1.ts
// except the on-disk directory.
import { BetaLocalFilesystemMemoryTool } from "@anthropic-ai/sdk/tools/memory/node";
async function main() {
const memory = await BetaLocalFilesystemMemoryTool.init("./memory-store");
console.log("--- view /memories (session 2, fresh process) ---");
console.log(await memory.view({ command: "view", path: "/memories" }));
console.log("--- view /memories/project_notes.md (session 2) ---");
console.log(
await memory.view({ command: "view", path: "/memories/project_notes.md" }),
);
const edit = await memory.str_replace({
command: "str_replace",
path: "/memories/project_notes.md",
old_str: "Postgres pool max is 10 connections in staging.",
new_str:
"Postgres pool max is 10 connections in staging, 25 in production (confirmed 2026-07-14).",
});
console.log("--- str_replace on project_notes.md ---");
console.log(edit);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
Real output:
--- view /memories (session 2, fresh process) ---
Here're the files and directories up to 2 levels deep in /memories, excluding hidden items and node_modules:
4K /memories
142B /memories/project_notes.md
--- view /memories/project_notes.md (session 2) ---
Here's the content of /memories/project_notes.md with line numbers:
1 # Project: invoice-service
2
3 - Postgres pool max is 10 connections in staging.
4 - Retry logic lives in src/retry.ts, NOT in the queue consumer.
5
--- str_replace on project_notes.md ---
The memory file has been edited. Here is the snippet showing the change (with line numbers):
1 # Project: invoice-service
2
3 - Postgres pool max is 10 connections in staging, 25 in production (confirmed 2026-07-14).
4 - Retry logic lives in src/retry.ts, NOT in the queue consumer.
5
The /memories directory's own reported size (4K above, unchanged before and after the write) reflects filesystem block allocation, not the byte size of what's inside it — re-running this on a different OS or filesystem can show a different number for that one line, including much smaller values on some setups. The per-file size (142B for project_notes.md) and everything else in these blocks come from actual content length and command output, so they're stable and will match what you see.
Session 2 saw exactly what session 1 wrote, with zero conversation history carried over — only the files on disk. That's the entire value proposition of the memory tool in one output block.
While testing this, I also checked something the docs don't explicitly confirm either way: does the ready-made filesystem implementation protect against path traversal, or is that only "your responsibility" if you write a custom handler? I called memory.view({ command: "view", path: "/memories/../../etc/passwd" }) against the same store, and it threw:
Error: Path /memories/../../etc/passwd would escape /memories directory
So BetaLocalFilesystemMemoryTool guards against traversal out of the box. Anthropic's security-considerations warning — "your implementation must validate every path in every command"1 — is aimed at people writing a custom handler (which Step 5 below covers), not a gap in the shipped reference implementation.
How does Claude know when to check its memory?
When the memory tool is present in a request's tools array, the API automatically appends an instruction to the system prompt telling Claude to view /memories before doing anything else and to assume its context could be reset at any moment1. You don't write this instruction yourself — it ships with the tool. That's also why the memory tool needs no beta header: it's a generally-available Messages API feature, not an opt-in beta1. Context Editing, covered next, is the opposite: it's still in beta and does require one.
Step 4: Turn on Context Editing so long runs don't exhaust their context window
A long-running agent that reads files, greps a codebase, and calls web search repeatedly will eventually fill its context window with tool results Claude has already processed and no longer needs. Context Editing's clear_tool_uses_20250919 strategy clears the oldest tool results once a threshold is crossed, replacing them with placeholder text so Claude knows something was removed2. Anthropic's own internal evaluation (an agentic-search benchmark, not independently reproducible) reported context editing alone improving performance by 29% over baseline, combining it with the memory tool reaching 39%, and a 100-turn web-search test finishing with 84% fewer tokens consumed than without it8 — directionally useful numbers for deciding whether this is worth wiring up, even though your own workload will differ. Enable it with a beta header and a context_management block:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const response = await client.beta.messages.create({
model: "claude-opus-4-8",
max_tokens: 4096,
messages: [{ role: "user", content: "Search for recent developments in AI" }],
tools: [{ type: "web_search_20250305", name: "web_search" }],
betas: ["context-management-2025-06-27"],
context_management: {
edits: [{ type: "clear_tool_uses_20250919" }],
},
});
The default configuration (trigger at 100,000 input tokens, keep the 3 most recent tool uses) is fine for a quick start, but the real configuration surface is worth knowing26:
| Option | Default | What it does |
|---|---|---|
trigger | 100,000 input tokens | When clearing starts — can be {type: "input_tokens", value: N} or {type: "tool_uses", value: N} |
keep | 3 tool uses | How many recent tool-use/result pairs survive a clearing pass |
clear_at_least | none | Minimum tokens that must be freed for a pass to run at all — protects against clearing that isn't worth invalidating your prompt cache for |
exclude_tools | none | Tool names that are never cleared (put "memory" here so Claude's own memory calls survive) |
clear_tool_inputs | false | Also clear the tool call parameters, not just results. The real type accepts a boolean or an array of specific tool names — the docs prose only mentions the boolean form6 |
Context Editing runs entirely server-side: it's applied before your prompt reaches Claude, and your client keeps the full, unedited conversation history locally. You never need to sync your local state to match what got cleared2.
What's the difference between Context Editing and Compaction?
Context Editing (clear_tool_uses_20250919) selectively deletes specific old tool results while leaving the rest of the conversation intact. Compaction (compact_20260112) is a different, coarser mechanism: it summarizes the entire conversation and replaces the full history with that summary once a token threshold is crossed2. Anthropic's own guidance is that server-side compaction is the primary strategy for most long conversations, and Context Editing is for scenarios needing finer-grained control over exactly what gets cleared2.
Here's a detail that's easy to miss if you're only reading docs prose instead of the SDK's actual type definitions: the BetaToolRunnerParams type has a compactionControl field, and its JSDoc says, verbatim:
@deprecated Use server-side compaction instead by passing
`edits: [{ type: 'compact_20260112' }]` in the params passed to `toolRunner()`.
See https://platform.claude.com/docs/en/build-with-claude/compaction
That's a real deprecation notice shipped in @anthropic-ai/sdk@0.111.0's own compiled types, not something inferred from a changelog6. If you find a tutorial (including older ones) showing compactionControl as the way to enable compaction, it's showing you a deprecated path — the current one is a compact_20260112 edit inside context_management, the same array where clear_tool_uses_20250919 lives.
Step 5: Combine memory and context editing in one agent
This is the pattern Anthropic recommends for genuinely long-running agents: Context Editing keeps the active context small automatically, and memory preserves anything that must survive a clearing pass, because the API sends Claude an automatic warning to save important information to memory before the threshold is crossed12. Wire both into client.beta.messages.toolRunner(), the SDK's built-in agent loop:
// full-agent-example.ts
import Anthropic from "@anthropic-ai/sdk";
import { betaMemoryTool } from "@anthropic-ai/sdk/helpers/beta/memory";
import { BetaLocalFilesystemMemoryTool } from "@anthropic-ai/sdk/tools/memory/node";
async function main() {
const client = new Anthropic();
const memory = await BetaLocalFilesystemMemoryTool.init("./memory-store");
const runner = client.beta.messages.toolRunner({
model: "claude-opus-4-8",
max_tokens: 4096,
messages: [
{
role: "user",
content:
"Review the invoice-service repo and note anything future sessions should know.",
},
],
tools: [betaMemoryTool(memory)],
betas: ["context-management-2025-06-27"],
context_management: {
edits: [
{
type: "clear_tool_uses_20250919",
trigger: { type: "input_tokens", value: 30000 },
keep: { type: "tool_uses", value: 3 },
clear_at_least: { type: "input_tokens", value: 5000 },
exclude_tools: ["memory"],
},
],
},
});
const finalMessage = await runner.runUntilDone();
console.log(finalMessage.content);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
Notice betaMemoryTool(memory) — you pass your BetaLocalFilesystemMemoryTool instance directly as the handlers object; betaMemoryTool() doesn't care whether it's backed by the local filesystem or something else, because both satisfy the same MemoryToolHandlers interface structurally. That's also why exclude_tools: ["memory"] matters here: without it, Claude's own memory writes could get cleared by the same pass that's supposed to protect information via memory in the first place.
If you need memory backed by something other than the local filesystem — S3, Postgres, one prefix per customer — write your own handler against the same interface instead of subclassing anything:
// custom-handler-pattern.ts — one memory store per customer, backed by
// whatever storage you already run (S3 shown as comments; swap in real calls).
import { betaMemoryTool, type MemoryToolHandlers } from "@anthropic-ai/sdk/helpers/beta/memory";
function scopedMemoryHandlers(customerId: string): MemoryToolHandlers {
const keyFor = (path: string) => `customers/${customerId}${path}`;
return {
view: async (cmd) => {
// const obj = await s3.getObject({ Key: keyFor(cmd.path) });
return `Here're the files and directories up to 2 levels deep in ${cmd.path}, excluding hidden items and node_modules:\n0B\t${cmd.path}`;
},
create: async (cmd) => {
// await s3.putObject({ Key: keyFor(cmd.path), Body: cmd.file_text });
return `File created successfully at: ${cmd.path}`;
},
str_replace: async () => `The memory file has been edited.`,
insert: async (cmd) => `The file ${cmd.path} has been edited.`,
delete: async (cmd) => `Successfully deleted ${cmd.path}`,
rename: async (cmd) => `Successfully renamed ${cmd.old_path} to ${cmd.new_path}`,
};
}
export function memoryToolFor(customerId: string) {
return betaMemoryTool(scopedMemoryHandlers(customerId));
}
Building a custom handler like this one puts multi-tenant isolation on you: scope every key by customer ID the way scopedMemoryHandlers() does above, and don't let one customer's memory reads or writes touch another's data. Anthropic's own guidance for the reference implementation covers the general cases that still apply here — strip sensitive data before writing, cap how large any one file or directory can grow, and periodically expire memory files nobody has read in a while1.
Verification
What was actually executed, in this sandbox, against the real installed package: session1.ts and session2.ts from Steps 2 and 3 were run for real with npx tsx, in two separate process invocations against the same ./memory-store directory — the console output shown in those steps is copy-pasted verbatim from those runs, not invented. The path-traversal test in Step 3 was also actually executed and actually threw the error shown. Every code block in this tutorial was then re-extracted verbatim from the finished markdown (not from the original working files) and re-checked as a final pass, the same way the two session scripts were re-run to confirm output. That re-extraction caught one real bug: the standalone Context Editing snippet in Step 4 uses top-level await and failed to compile with TS1309 until "type": "module" was added to package.json — now folded into Step 1 above. With that fix, all five extracted TypeScript blocks type-check together with tsc --strict --noUncheckedIndexedAccess against the real, npm-installed @anthropic-ai/sdk@0.111.0 — clean compile, zero errors, no skipLibCheck workaround needed. Re-running the session scripts a second time, in a separate clean sandbox, caught one more thing worth knowing: the /memories directory's own reported size came back as 4K instead of the first run's 64B/96B. That's a filesystem-block-allocation artifact, not a bug — so the figures shown in Steps 2 and 3 now reflect that second, independently-reproduced run, and everything else in those output blocks (file contents, byte counts for actual files, success messages, the traversal error) matched byte-for-byte across both runs.
What was not executed: the live client.beta.messages.create() and toolRunner() calls in Steps 4 and 5, which require a paid ANTHROPIC_API_KEY. This automated writing pipeline has no API key configured. If you have one, run:
export ANTHROPIC_API_KEY=sk-ant-...
npx tsx full-agent-example.ts
You should see Claude write at least one file under ./memory-store/memories/ before finishing, and — if the conversation is long enough to cross the 30,000-token trigger — a context_management.applied_edits array in the response showing cleared_tool_uses and cleared_input_tokens counts2.
Troubleshooting
view on /memories returns an error instead of an empty listing. If you wrote your own handler instead of using BetaLocalFilesystemMemoryTool, make sure your view implementation treats a missing/empty memory root as a valid empty directory, not a "path does not exist" error — only BetaLocalFilesystemMemoryTool creates the root automatically before Claude's first call1.
Context Editing silently does nothing. Check that betas: ["context-management-2025-06-27"] is actually in the request — it's a beta header, not a default-on feature, unlike the memory tool2.
Memory writes get cleared before Claude can use them again. Add "memory" to exclude_tools in your clear_tool_uses_20250919 config. Without it, a clearing pass can remove the very tool_result blocks documenting what Claude just saved2.
tsc --strict fails on a NodeJS namespace error. This isn't a bug in @anthropic-ai/sdk — it means @types/node isn't installed. Run npm install -D @types/node@26.1.1 and it resolves; verified directly in this sandbox.
A tutorial or older blog post shows compactionControl and it doesn't work. That field is deprecated in the current SDK. Use context_management: { edits: [{ type: "compact_20260112" }] } passed to toolRunner() instead6.
Security considerations
Everything the Claude memory tool does runs through code you write and host — Claude only requests file operations; your application executes them1. Three things worth building in from the start, straight from Anthropic's own guidance: strip or reject attempts to write obviously sensitive data (API keys, credentials) to memory files even though Claude usually refuses to write them unprompted; cap how large a memory file or directory can grow, and let Claude page through long files with view_range instead of dumping everything at once; and periodically expire memory files nobody has accessed in a long time1. If you're writing a custom handler instead of using BetaLocalFilesystemMemoryTool, path traversal protection is entirely on you — validate that every path resolves inside your intended root before touching real storage.
Next steps and further reading
This tutorial builds directly on client.beta.messages.toolRunner(), which wraps the same tool_use/tool_result loop covered from first principles in the Claude tool use agentic loop tutorial — worth reading if you want to understand what toolRunner() is doing under the hood, or if you need to hand-roll the loop for a language without a runner helper. If your agent needs a human to approve risky actions in addition to remembering things across sessions, the human-in-the-loop Claude Agent SDK tutorial covers canUseTool callbacks and PreToolUse hooks on top of the (different) Agent SDK package. And if hosting your own memory storage isn't something you want to own operationally, Claude Managed Agents: Dreaming, Outcomes, Orchestration covers Anthropic's hosted alternative, where memory curation happens server-side instead of in your own handler.
For the underlying design principles behind when to reach for memory versus context editing versus compaction, Anthropic's own engineering write-up on effective context engineering is the primary source this tutorial's docs citations point back to9.
Footnotes
-
Anthropic, "Memory tool" — https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool (GA status/no beta header, command reference, empty-directory view behavior, prompting guidance, security considerations, production patterns; fetched 2026-07-14) ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12
-
Anthropic, "Context editing" — https://platform.claude.com/docs/en/build-with-claude/context-editing (beta header,
clear_tool_uses_20250919config options and defaults, server-side execution model, compaction comparison; fetched 2026-07-14) ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 -
Node.js release data — https://endoflife.date/api/nodejs.json (structured API, fetched directly 2026-07-14): Node 24 active-support window 2025-10-28 to 2026-10-20 (Active LTS as of this writing), then maintenance until EOL 2028-04-30; Node 22 active-support ended 2025-10-21 (Maintenance LTS as of this writing), EOL 2027-04-30; Node 26 latest release 26.5.0, not yet LTS-promoted (LTS conversion scheduled 2026-10-28), EOL 2029-04-30 ↩
-
npm registry — https://registry.npmjs.org/@anthropic-ai/sdk/latest (version 0.111.0, published 2026-07-10T17:14:13.049Z); https://registry.npmjs.org/typescript/latest (7.0.2); https://registry.npmjs.org/tsx/latest (4.23.1); https://registry.npmjs.org/@types/node/latest (26.1.1) — all fetched directly from the registry API, not a search summary, 2026-07-14 ↩ ↩2
-
Claude Platform pricing — https://platform.claude.com/docs/en/about-claude/pricing (Claude Opus 4.8: $5/MTok input, $25/MTok output, flat pricing with no introductory-rate split; fetched 2026-07-14) ↩
-
@anthropic-ai/sdk@0.111.0compiled type definitions —helpers/beta/memory.d.ts,tools/memory/node.d.ts,lib/tools/BetaToolRunner.d.ts,resources/beta/messages/messages.d.ts— installed from npm and inspected directly, 2026-07-14 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 -
@anthropic-ai/claude-agent-sdk@0.3.150compiled type definitions, as documented in the 2026-07-13 human-in-the-loop tutorial's own Verification section — a different package from the one used in this tutorial ↩ -
Anthropic, "Managing context on the Claude Developer Platform" — https://claude.com/blog/context-management (announcement dated September 29, 2025; 39% combined / 29% context-editing-alone performance improvement and 84% token reduction in a 100-turn evaluation — Anthropic's own internal agentic-search evaluation set, not independently reproducible; fetched 2026-07-14) ↩
-
Anthropic Engineering, "Effective context engineering for AI agents" — https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents (linked from the memory tool docs page as the broader design-principles reference; fetched 2026-07-14) ↩


