# Claude's memory tool

> Keep the notes Claude's memory tool writes in each user's Geniffy memory. Every file comes back exactly as Claude wrote it, and what it says is recalled by context() and ask() anywhere in your app.

Claude's [memory tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/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

```bash
pip install "geniffy[claude]"
```

```bash
npm install @anthropic-ai/sdk geniffy
```

`geniffy[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

```python
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")
```

```ts
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.

```python
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?")
```

```ts
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`:

```python
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 it
```

```ts
const 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:

```python
print(mem.context("How does this user want us to follow up?"))
```

```ts
console.log(await mem.context("How does this user want us to follow up?"));
```

Each line names the file it came from:

```text
- 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](https://docs.geniffy.com/add-memories/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](https://docs.geniffy.com/integrations/anthropic) page, and give Claude the memory tool for its own notes:

```python
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()
```

```ts
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.** `view` lists a directory or reads a file; `create` writes a file
  whole, replacing any file already at that path, as Claude's tool description says it does; `str_replace` and
  `insert` read the file, change it and write it back; `delete` takes a file or a whole directory; `rename`
  moves 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.** `view` lists two levels deep, leaving out hidden names and
  `node_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 `/memories` itself. 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

```python
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 all
```

```ts
import { 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 all
```

The 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](https://docs.geniffy.com/correct-and-forget).

## With asyncio

With `AsyncAnthropic` in Python, use `AsyncGeniffyMemoryTool` on an `AsyncGeniffy` client:

```python
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](https://docs.geniffy.com/add-memories/files-kept-exactly) for the rules, and the [reference](https://docs.geniffy.com/api/files) for
every field.

Source: https://docs.geniffy.com/integrations/claude-memory-tool
