Claude's memory tool
Claude's memory tool lets Claude
keep notes as files in a /memories directory and read them back in later conversations. Claude asks for each
file operation and your app carries it out, so your app decides where the files live. Keep them in Geniffy and
each user's Claude keeps its notes in that user's memory: every file comes back exactly as Claude wrote it, and
what it says is learned as well, so context() and ask() recall it anywhere else in your app.
Why keep the files in Geniffy
- Nothing to host. No disk, bucket or table of your own for the files, and nothing to keep in step with your users.
- Each file exactly as Claude wrote it. Claude edits a file by quoting a run of its text, so the text has to come back character for character. Geniffy keeps it that way, spaces and line endings included.
- What Claude wrote is memory too. Each file is learned like a note titled by its path, so a support screen, a nightly job or a model from another provider can recall what Claude noted, with the file each line came from.
- One memory per user. The tool works on a client bound to one of your users with
space(), so one user's notes never reach another user's Claude. - Gone when the user goes. Deleting a file takes back what only it taught, forgetting a user takes their files with everything else, and the copy you give a user who asks what you hold includes every file.
Install
pip install "geniffy[claude]"npm install @anthropic-ai/sdk geniffygeniffy[claude] installs the anthropic package as well (1.0 or later, on Python 3.10 or later), which
geniffy.claude builds on; import geniffy on its own still needs nothing more. In TypeScript, geniffy/claude
needs @anthropic-ai/sdk 0.72 or later, and the main geniffy entry still has no dependencies. Set
ANTHROPIC_API_KEY, and GENIFFY_API_KEY from API keys in the Geniffy app.
Give each user's Claude a memory
import anthropic
from geniffy import Geniffy
from geniffy.claude import GeniffyMemoryTool
claude = anthropic.Anthropic() # reads ANTHROPIC_API_KEY
geniffy = Geniffy() # reads GENIFFY_API_KEY
def chat(user_id: str, message: str) -> str:
# This user's Claude keeps its notes in this user's memory, and no one else's.
memory = GeniffyMemoryTool(geniffy.space(f"user_{user_id}"))
final = claude.beta.messages.tool_runner(
model="claude-opus-5-5",
max_tokens=16000,
tools=[memory],
messages=[{"role": "user", "content": message}],
).until_done()
return "".join(block.text for block in final.content if block.type == "text")import Anthropic from "@anthropic-ai/sdk";
import { betaMemoryTool } from "@anthropic-ai/sdk/helpers/beta/memory";
import { Geniffy } from "geniffy";
import { geniffyMemoryHandlers } from "geniffy/claude";
const claude = new Anthropic(); // reads ANTHROPIC_API_KEY
const geniffy = new Geniffy(); // reads GENIFFY_API_KEY
export async function chat(userId: string, message: string) {
// This user's Claude keeps its notes in this user's memory, and no one else's.
const mem = geniffy.space(`user_${userId}`);
const memory = betaMemoryTool(geniffyMemoryHandlers(mem));
const final = await claude.beta.messages
.toolRunner({
model: "claude-opus-5-5",
max_tokens: 16000,
tools: [memory],
messages: [{ role: "user", content: message }],
})
.runUntilDone();
return final.content
.flatMap((block) => (block.type === "text" ? [block.text] : []))
.join("");
}The tool runner carries out each command Claude sends and keeps going until Claude has its answer. The memory tool itself needs no beta header, and it works with every Claude model from Claude 4 on.
With the memory tool in a request, Claude looks at its memory directory before it starts. The first time it finds nothing and writes down what is worth keeping; the next conversation, a minute or a month later, starts by reading that back.
chat("1042", "Remember that I prefer email follow-ups, not calls.")
# a later conversation: Claude reads /memories first
chat("1042", "How should you follow up with me?")await chat("1042", "Remember that I prefer email follow-ups, not calls.");
// a later conversation: Claude reads /memories first
await chat("1042", "How should you follow up with me?");See what Claude wrote
Claude names its own files, such as /memories/preferences.md. They are that user's files under /memories:
mem = geniffy.space("user_1042")
for f in mem.files.list("/memories/").files: # by path
print(f.path, f.size, f.updated_at) # size in characters
print(mem.files.get(f.path).text) # exactly as Claude wrote itconst mem = geniffy.space("user_1042");
const { files } = await mem.files.list({ prefix: "/memories/" }); // by path
for (const f of files) {
console.log(f.path, f.size, f.updated_at); // size in characters
console.log((await mem.files.get(f.path)).text); // exactly as Claude wrote it
}Recall what Claude wrote
Anywhere in your app, context() and ask() recall what Claude noted, beside everything else you added for
that user:
print(mem.context("How does this user want us to follow up?"))console.log(await mem.context("How does this user want us to follow up?"));Each line names the file it came from:
- Prefers email follow-ups, not calls. [/memories/preferences.md, 2026-10-06]Every file the tool writes carries the label channel: claude-memory, so a recall can keep to Claude's notes:
mem.context(question, labels={"channel": "claude-memory"}) in Python, or mem.context(question, { labels:
LABELS }) with LABELS from geniffy/claude in TypeScript. See Labels.
With recall first
The memory tool holds what Claude chose to note. Recall first puts everything else your app knows about the user
in front of Claude as well: what they said in other conversations, and anything you synced from their email,
documents or tickets. The two work together. Put context() in the system prompt, as on the
Anthropic page, and give Claude the memory tool for its own notes:
mem = geniffy.space(f"user_{user_id}")
context = mem.context(message) # what bears on the message
final = claude.beta.messages.tool_runner(
model="claude-opus-5-5",
max_tokens=16000,
system=f"You are a helpful assistant.\n\n<memory>\n{context}\n</memory>",
tools=[GeniffyMemoryTool(mem)],
messages=[{"role": "user", "content": message}],
).until_done()const mem = geniffy.space(`user_${userId}`);
const context = await mem.context(message); // what bears on the message
const final = await claude.beta.messages
.toolRunner({
model: "claude-opus-5-5",
max_tokens: 16000,
system: `You are a helpful assistant.\n\n<memory>\n${context}\n</memory>`,
tools: [betaMemoryTool(geniffyMemoryHandlers(mem))],
messages: [{ role: "user", content: message }],
})
.runUntilDone();How it behaves
- Each command is a call to the files API.
viewlists a directory or reads a file;createwrites a file whole, replacing any file already at that path, as Claude's tool description says it does;str_replaceandinsertread the file, change it and write it back;deletetakes a file or a whole directory;renamemoves a file or a directory, and never onto a path that is taken. - An edit learns only what changed. A file written again is the same source, so only the paragraphs that changed are learned, and what was removed is taken back. A renamed file keeps its text and what it taught: nothing is learned again. An empty file is kept, with nothing learned from it.
- A directory is the files under it.
viewlists two levels deep, leaving out hidden names andnode_modules, with sizes in characters; a directory's size is what its files hold. - Paths stay inside
/memories. A path that leads out of it is refused, typed or URL-encoded (..,%2e%2e, a backslash), and so is deleting or renaming/memoriesitself. Doubled and trailing slashes are dropped. - Claude hears what went wrong. A command that can't be carried out, such as a missing file, text that isn't there or a line past the end, goes back to Claude as an error result in the words of Anthropic's memory tool documentation, so it can try again. So does a write Geniffy refuses, such as a file over 2,000,000 characters, in Geniffy's words. The tool runner hands any other failure to Claude the same way, such as a revoked key or Geniffy out of reach, rather than raising it in your app; the Python runner logs it as well.
Reset, forget and export
mem = geniffy.space("user_1042")
mem.export() # their copy, every file listed by path
GeniffyMemoryTool(mem).clear_all_memory() # deletes every file under /memories
geniffy.forget_space("user_1042") # forgets the user, files and allimport { clearAllMemory } from "geniffy/claude";
const mem = geniffy.space("user_1042");
await mem.export(); // their copy, every file listed by path
await clearAllMemory(mem); // deletes every file under /memories
await geniffy.forgetSpace("user_1042"); // forgets the user, files and allThe copy lists each file by its path, size and when it was last written; files.get(path) hands back its text,
exactly as Claude wrote it, so the copy stays small however much the files hold. Clearing takes back what only
those files taught, too. It is for your app to call, such as from a button that resets the assistant: it isn't one
of the tool's commands, so Claude can't call it. See Correct and forget.
With asyncio
With AsyncAnthropic in Python, use AsyncGeniffyMemoryTool on an AsyncGeniffy client:
from anthropic import AsyncAnthropic
from geniffy import AsyncGeniffy
from geniffy.claude import AsyncGeniffyMemoryTool
memory = AsyncGeniffyMemoryTool(AsyncGeniffy().space(f"user_{user_id}"))
final = await AsyncAnthropic().beta.messages.tool_runner(
model="claude-opus-5-5", max_tokens=16000, tools=[memory],
messages=[{"role": "user", "content": message}],
).until_done()The files underneath
The tool is built on Geniffy's files API, which works the same for any agent that keeps its own notes as files, with any model:
| Call | What it does |
|---|---|
PUT /v1/files |
Writes a file, {"path", "text", "labels"}: kept exactly, and learned like a note |
GET /v1/files?path= |
One file, with its text exactly as written |
GET /v1/files?prefix= |
The files under a prefix, in path order |
DELETE /v1/files?path= |
A file, its text and what only it taught; ?prefix= for every file under one |
POST /v1/files/move |
{"from", "to"}: a file, or every file beneath a directory |
See Files kept exactly for the rules, and the reference for every field.