Skip to main content

Claude Memory Tool in Practice: The Complete Guide to Giving Your Agent Cross-Session Memory

Claude's memory tool is a client-side storage-driven persistent memory capability. With just 6 commands, it enables cross-session information retention — and paired with context editing, benchmarks show 84% token savings on long-running tasks. This post covers the underlying mechanism, a minimal working Python implementation, how it differs from RAG, security best practices, and how to access it via API.

Dev GuidesmemoryMemoryEst. read5min
2026.05.22 published
Claude Memory Tool in Practice: The Complete Guide to Giving Your Agent Cross-Session Memory

Claude Memory Tool in Practice: The Complete Guide to Giving Your Agent Cross-Session Memory

Anyone who’s built an Agent has hit this wall: the user says “I prefer dark mode and short answers” in session one, and the model has zero recollection by session two. Stuffing preferences into the system prompt is a band-aid. Building your own RAG pipeline is a mountain of engineering.

On September 29, 2025, Anthropic released the memory_20250818 tool (official docs) — a much lighter approach than rolling your own RAG. It lets Claude treat memory as local files it can read and write. Combined with context editing, Anthropic’s internal benchmarks on a 100-turn web search task showed 84% token savings and a 39% performance improvement.

This post walks through the full picture: mechanism → command schema → minimal implementation → comparison with RAG → security → API access. Complete, copy-paste-ready code is included at the end.

1. Core Mechanism: Client-Side Storage with a File System Metaphor

Understanding the memory tool comes down to one sentence: the model only issues read/write instructions — the actual storage lives on your side.

This architecture delivers three immediate benefits:

Dimension What It Means
Data sovereignty User data never leaves your server. Where it’s stored, whether it’s encrypted, how long it’s retained — all your call
Auditability Every memory operation is a visible tool_use/tool_result block that you can log directly
Storage flexibility Local files, PostgreSQL, Redis, S3, encrypted volumes — just implement one abstract class

There’s a deliberate reason Anthropic designed memory as a “file system” rather than a “key-value store” — Claude has already been trained extensively on file operations (view / create / str_replace), so no additional instruction is needed. Anthropic also auto-injects this into the system prompt: “IMPORTANT: ALWAYS VIEW YOUR MEMORY DIRECTORY BEFORE DOING ANYTHING ELSE.” — meaning the model proactively checks memory at the start of every session.

2. The 6 Commands: Full Schema and Tool Result Conventions

The memory tool’s command set is deliberately minimal — just 6 commands, but they cover everything. Here’s the JSON structure the model will send you, and the response format you need to follow:

2.1 view — Read a Directory or File

{
  "command": "view",
  "path": "/memories",
  "view_range": [1, 10]
}
{
  "command": "view",
  "path": "/memories",
  "view_range": [1, 10]
}
  • If the path is a directory → return the file listing (with sizes)
  • If the path is a file → return content; view_range is optional, 1-based, closed interval
  • view_range: [-1, -1] means the last line

2.2 create — Create or Overwrite a File

{
  "command": "create",
  "path": "/memories/preferences.md",
  "file_text": "## User Preferences\n- Theme: dark\n- Verbose: false\n"
}
{
  "command": "create",
  "path": "/memories/preferences.md",
  "file_text": "## User Preferences\n- Theme: dark\n- Verbose: false\n"
}

Creates if it doesn’t exist, overwrites entirely if it does — so when Claude wants to “append,” it actually does a view first, then a create.

2.3 str_replace — Exact String Replacement

{
  "command": "str_replace",
  "path": "/memories/preferences.md",
  "old_str": "Theme: dark",
  "new_str": "Theme: light"
}
{
  "command": "str_replace",
  "path": "/memories/preferences.md",
  "old_str": "Theme: dark",
  "new_str": "Theme: light"
}

old_str must be unique within the file. If not, return an error:

No replacement was performed. Multiple occurrences of old_str ... Please ensure it is unique

If you don’t match this exact text format, the model will retry and won’t converge.

2.4 insert — Insert at a Specific Line

{
  "command": "insert",
  "path": "/memories/todo.md",
  "insert_line": 2,
  "insert_text": "- [ ] Review memory tool docs\n"
}
{
  "command": "insert",
  "path": "/memories/todo.md",
  "insert_line": 2,
  "insert_text": "- [ ] Review memory tool docs\n"
}

insert_line is 0-based (inserting before line 0 = beginning of file). On out-of-bounds, return:

Error: Invalid insert_line parameter: ... It should be within the range of lines of the file: [0, N]

2.5 delete — Recursive Delete

{ "command": "delete", "path": "/memories/old-notes" }
{ "command": "delete", "path": "/memories/old-notes" }

Deletes files directly, directories recursively — which is why path validation is a critical security boundary (see Section 6).

2.6 rename — Rename or Move

{ "command": "rename", "old_path": "/memories/draft.md", "new_path": "/memories/final.md" }
{ "command": "rename", "old_path": "/memories/draft.md", "new_path": "/memories/final.md" }

Must return an error if new_path already exists — no silent overwriting:

Error: The destination /memories/final.md already exists

3. Minimal Working Implementation: 30 Lines of Python

Here’s a minimal example you can actually run (using local file storage). For production, consider using the SDK’s BetaAbstractMemoryTool abstract class, but start with the plain version to understand the mechanics:

import os
import json
from pathlib import Path
from anthropic import Anthropic

MEMORY_ROOT = Path("./agent_memory").resolve()
MEMORY_ROOT.mkdir(exist_ok=True)

def safe_path(p: str) -> Path:
    """Prevent directory traversal: /memories/foo → ./agent_memory/foo, must stay under ROOT"""
    rel = p.lstrip("/").removeprefix("memories/").removeprefix("memories")
    target = (MEMORY_ROOT / rel).resolve()
    target.relative_to(MEMORY_ROOT)  # Raises ValueError if traversal detected
    return target

def handle_memory(cmd: dict) -> str:
    op = cmd["command"]
    path = safe_path(cmd["path"])

    if op == "view":
        if path.is_dir():
            return "\n".join(f"{f.name} ({f.stat().st_size}B)" for f in path.iterdir())
        text = path.read_text(encoding="utf-8")
        if "view_range" in cmd:
            lines = text.splitlines()
            a, b = cmd["view_range"]
            text = "\n".join(lines[a-1:b])
        return text

    if op == "create":
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(cmd["file_text"], encoding="utf-8")
        return f"File created: {cmd['path']}"

    if op == "str_replace":
        text = path.read_text(encoding="utf-8")
        if text.count(cmd["old_str"]) != 1:
            return f"No replacement performed. Found {text.count(cmd['old_str'])} occurrences."
        path.write_text(text.replace(cmd["old_str"], cmd["new_str"]), encoding="utf-8")
        return "Replaced."

    if op == "insert":
        lines = path.read_text(encoding="utf-8").splitlines(keepends=True)
        i = cmd["insert_line"]
        if not 0 <= i <= len(lines):
            return f"Error: Invalid insert_line {i}. Range [0, {len(lines)}]."
        lines.insert(i, cmd["insert_text"])
        path.write_text("".join(lines), encoding="utf-8")
        return "Inserted."

    if op == "delete":
        if path.is_dir():
            import shutil; shutil.rmtree(path)
        else:
            path.unlink()
        return "Deleted."

    if op == "rename":
        new = safe_path(cmd["new_path"])
        if new.exists():
            return f"Error: The destination {cmd['new_path']} already exists"
        path.rename(new)
        return "Renamed."

    return f"Unknown command: {op}"


client = Anthropic(
    api_key=os.environ["CLAUDEAPI_KEY"],
    base_url="https://gw.claudeapi.com",
)

messages = [{"role": "user", "content": "Remember this: I prefer short answers and dark mode."}]

while True:
    resp = client.beta.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=2048,
        betas=["context-management-2025-06-27"],
        tools=[{"type": "memory_20250818", "name": "memory"}],
        messages=messages,
    )

    if resp.stop_reason != "tool_use":
        print(resp.content[0].text)
        break

    # Execute memory commands issued by the model
    tool_results = []
    for block in resp.content:
        if block.type == "tool_use" and block.name == "memory":
            result = handle_memory(block.input)
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": result,
            })

    messages.append({"role": "assistant", "content": resp.content})
    messages.append({"role": "user", "content": tool_results})
import os
import json
from pathlib import Path
from anthropic import Anthropic

MEMORY_ROOT = Path("./agent_memory").resolve()
MEMORY_ROOT.mkdir(exist_ok=True)

def safe_path(p: str) -> Path:
    """Prevent directory traversal: /memories/foo → ./agent_memory/foo, must stay under ROOT"""
    rel = p.lstrip("/").removeprefix("memories/").removeprefix("memories")
    target = (MEMORY_ROOT / rel).resolve()
    target.relative_to(MEMORY_ROOT)  # Raises ValueError if traversal detected
    return target

def handle_memory(cmd: dict) -> str:
    op = cmd["command"]
    path = safe_path(cmd["path"])

    if op == "view":
        if path.is_dir():
            return "\n".join(f"{f.name} ({f.stat().st_size}B)" for f in path.iterdir())
        text = path.read_text(encoding="utf-8")
        if "view_range" in cmd:
            lines = text.splitlines()
            a, b = cmd["view_range"]
            text = "\n".join(lines[a-1:b])
        return text

    if op == "create":
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(cmd["file_text"], encoding="utf-8")
        return f"File created: {cmd['path']}"

    if op == "str_replace":
        text = path.read_text(encoding="utf-8")
        if text.count(cmd["old_str"]) != 1:
            return f"No replacement performed. Found {text.count(cmd['old_str'])} occurrences."
        path.write_text(text.replace(cmd["old_str"], cmd["new_str"]), encoding="utf-8")
        return "Replaced."

    if op == "insert":
        lines = path.read_text(encoding="utf-8").splitlines(keepends=True)
        i = cmd["insert_line"]
        if not 0 <= i <= len(lines):
            return f"Error: Invalid insert_line {i}. Range [0, {len(lines)}]."
        lines.insert(i, cmd["insert_text"])
        path.write_text("".join(lines), encoding="utf-8")
        return "Inserted."

    if op == "delete":
        if path.is_dir():
            import shutil; shutil.rmtree(path)
        else:
            path.unlink()
        return "Deleted."

    if op == "rename":
        new = safe_path(cmd["new_path"])
        if new.exists():
            return f"Error: The destination {cmd['new_path']} already exists"
        path.rename(new)
        return "Renamed."

    return f"Unknown command: {op}"


client = Anthropic(
    api_key=os.environ["CLAUDEAPI_KEY"],
    base_url="https://gw.claudeapi.com",
)

messages = [{"role": "user", "content": "Remember this: I prefer short answers and dark mode."}]

while True:
    resp = client.beta.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=2048,
        betas=["context-management-2025-06-27"],
        tools=[{"type": "memory_20250818", "name": "memory"}],
        messages=messages,
    )

    if resp.stop_reason != "tool_use":
        print(resp.content[0].text)
        break

    # Execute memory commands issued by the model
    tool_results = []
    for block in resp.content:
        if block.type == "tool_use" and block.name == "memory":
            result = handle_memory(block.input)
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": result,
            })

    messages.append({"role": "assistant", "content": resp.content})
    messages.append({"role": "user", "content": tool_results})

Run this once and you’ll see a preferences.md (or similarly named file) created under ./agent_memory/. On the next run of the same script, the model will automatically view the directory, read the preferences, and respond accordingly.

Note: betas=["context-management-2025-06-27"] is required — without it, the API will return a beta-not-enabled error. The model must be one of: Sonnet 4.5/4.6, Haiku 4.5, Opus 4.1/4.6/4.7.

4. Pairing with Context Editing: Where the 84% Token Savings Come From

The memory tool is useful on its own, but combining it with context editing is what produces Anthropic’s reported “39% improvement / 84% token savings”:

resp = client.beta.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=2048,
    betas=["context-management-2025-06-27"],
    tools=[{"type": "memory_20250818", "name": "memory"}],
    context_management={
        "edits": [{
            "type": "clear_tool_uses_20250919",
            "trigger": {"type": "input_tokens", "value": 100000},
            "keep": {"type": "tool_uses", "value": 3},
            "exclude_tools": ["memory"],  # Key: don't clear memory operations
        }]
    },
    messages=messages,
)
resp = client.beta.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=2048,
    betas=["context-management-2025-06-27"],
    tools=[{"type": "memory_20250818", "name": "memory"}],
    context_management={
        "edits": [{
            "type": "clear_tool_uses_20250919",
            "trigger": {"type": "input_tokens", "value": 100000},
            "keep": {"type": "tool_uses", "value": 3},
            "exclude_tools": ["memory"],  # Key: don't clear memory operations
        }]
    },
    messages=messages,
)

The logic is straightforward: in long-running tasks, automatically clear “one-shot tool results” from search/web/file operations, keeping only the most recent 3 plus all memory operations. The model persists important findings to memory files and views them again when needed, instead of letting the context window get blown up by dozens of tool_use blocks.

This combination gives an Agent the ability to “learn as it works” for the first time — in a 100-turn search task, traditional approaches would have failed long ago from context overflow.

5. Memory Tool vs RAG vs Managed Agents Memory: How to Choose

New features can be confusing — how does this relate to existing approaches? Here’s a comparison:

Dimension Memory Tool (this post) Self-Built RAG Managed Agents Memory
Trigger Model proactively view/create App layer retrieves and injects first Auto-mounted to session directory
Storage medium Client-side, your choice Vector DB (Chroma/PG/Pinecone) Anthropic-hosted
Retrieval granularity File-level Chunk + similarity File-level (versioned)
User isolation You manage directories You add metadata Workspace-scoped, built-in
Complexity ★★ (inherit one class) ★★★★ (embed + retrieve + rerank) ★ (API call)
Best for Agent preferences / notes / project state Large-scale knowledge retrieval Don’t want to manage storage

The short version:

  • Personalization, state, notes → Memory Tool
  • Large corpus knowledge retrieval → RAG (memory tool isn’t designed for this; it works fine under a few hundred KB, but don’t try it with gigabytes)
  • Don’t want to manage storage + using the Managed Agents platform → Managed Agents Memory (docs)

Memory tool and RAG aren’t mutually exclusive — they stack well: RAG handles “knowledge,” memory tool handles “personalization and process artifacts.”

6. Four Security Measures You Must Implement

The memory tool gives the model direct file operations. Security matters more than features here. Do at least these four things:

A. Enforce Path Validation

The single most important measure. Look at the safe_path() function above — it uses Path.resolve() + relative_to(MEMORY_ROOT), so a malicious path like /memories/../../../etc/passwd will trigger a relative_to exception. Never validate paths by string concatenation.

B. Set Size Limits

Cap individual files (recommended ≤ 64KB) and total directory size (recommended ≤ 10MB per user). Claude may aggressively append during long tasks — without limits, it’s a ticking time bomb.

C. Filter Sensitive Information

Filter obviously sensitive fields before writes (credit card numbers, passwords, optionally emails), or require explicit user consent. Memory is persistent, which means GDPR and other data protection regulations apply.

D. Isolate Users

Give each user an independent root directory keyed by user_id. The model must never be able to access across users. For multi-tenant setups, just change MEMORY_ROOT to BASE / user_id.

7. API Access: One Config Change with claudeapi.com

Accessing Anthropic’s API directly can involve network reliability challenges and account restrictions depending on your region. The memory tool’s long-session pattern demands especially stable connectivity.

claudeapi.com provides a globally accessible API gateway that’s fully compatible with the Anthropic SDK, including beta headers and the memory_20250818 tool. Just point your base_url:

client = Anthropic(
    api_key="sk-your-key",
    base_url="https://gw.claudeapi.com",
)
client = Anthropic(
    api_key="sk-your-key",
    base_url="https://gw.claudeapi.com",
)

Supported models (all memory tool-compatible) and pricing:

Model ID Input Output Good for Memory Agents?
claude-opus-4-7 $15/M tokens $75/M tokens ✅ Complex decision-making Agents
claude-opus-4-6 $15/M tokens $75/M tokens ✅ Same tier, fallback option
claude-sonnet-4-6 $3/M tokens $15/M tokens ✅✅ Default pick — best value
claude-haiku-4-5-20251001 $0.80/M tokens $4/M tokens ✅ High-frequency simple memory reads/writes

Real-world cost example: For a customer support Agent averaging 8 turns per session, ~3,000 input tokens + 500 output tokens per turn + 2 memory operations (~1,500 tokens), using Sonnet 4.6, the cost per session is roughly $0.02. With prompt caching, you can cut that by another 50%.

New users get trial credits: console.claudeapi.com — get your key in 5 minutes, run the 30-line code from Section 3, and you’ll have an Agent that remembers user preferences across sessions.

8. Production Readiness Checklist

One final checklist to run through before going to production:

  • [ ] Path validation uses Path.resolve() + relative_to(), not string concatenation
  • [ ] Size limits on both individual files and total directory
  • [ ] Multi-tenant isolation by user_id at the root directory level
  • [ ] Sensitive field filtering or encryption before writes
  • [ ] Context editing with clear_tool_uses_20250919 + exclude_tools: ["memory"]
  • [ ] Audit logging: command + target file + user for every memory operation
  • [ ] Periodic cleanup of files not accessed in 90+ days
  • [ ] Use the SDK’s BetaAbstractMemoryTool abstraction instead of raw dicts

The memory tool compresses “giving your Agent long-term memory” from “build a RAG pipeline” down to “implement a file system interface.” For small and mid-sized teams, this might be the most valuable capability to spend an afternoon learning in 2026.

Related Articles