Add Memory to Claude Code¶
Claude Code is the reference wiring. It has the richest verified hook contract, a packaged two-command install, and it is the harness this integration's payload fixtures were captured from live.
Pre-release
popoto[mcp] is not on PyPI yet. Until a release ships, install from a
checkout: pip install -e '.[mcp]'. Hooks work on a bare
pip install popoto; the extra is only for the MCP tools.
Install¶
doctor must print redis reachable and retrieval lexical
(query-sensitive) before you go further. If Redis is not running:
On a PostgreSQL 18+ server instead, export POPOTO_BACKEND=postgres and
POPOTO_POSTGRES_URL where Claude Code's hooks will see them (doctor then
prints postgres reachable and needs no Redis at all); see
On Postgres.
Then either the plugin or the manual block. They do the same thing.
Plugin¶
Manual¶
Add to ~/.claude/settings.json:
{
"hooks": {
"UserPromptSubmit": [
{ "hooks": [{ "type": "command", "command": "popoto-memory hook" }] }
],
"Stop": [
{ "hooks": [{ "type": "command", "command": "popoto-memory hook", "async": true }] }
]
}
}
Eight lines. "async": true on Stop keeps the write off the turn's
critical path.
To scope memory to one project rather than to ~/.claude/settings.json
globally, put the same block in that project's .claude/settings.json.
Restart Claude Code either way.
Verify¶
Have a conversation that establishes a fact, then start a new session and ask about it.
you: we deploy blue-green, and a failed health check rolls back automatically
claude: ...
(new session)
you: what happens if a deploy fails its health check?
claude: it rolls back automatically to the previous green environment
The recall happened because UserPromptSubmit fired, not because Claude
decided to look something up.
Then:
last assemble and last capture carry recent timestamps, records is
climbing, and failures reads none. Those three lines are the whole
health check.
What is stored, and where¶
In your Redis or Valkey, in the DefaultMemory:* keyspace (on Postgres: the
default_memory table of POPOTO_POSTGRES_SCHEMA), tagged with an
agent_id that defaults to the basename of your working directory. Since
1.9.0 that tag is honored as a read filter on every shipped retrieval path,
the default lexical/BM25 one included (#576).
On 1.8.2 and earlier it was honored only on the composite-score path, so two
projects on one Redis could retrieve each other's memories; a distinct
POPOTO_MEMORY_URL database per project was the workaround, and remains the
stronger boundary if you want one enforced by Redis rather than by a query
filter.
The store can be Redis, Valkey or PostgreSQL 18+. For Postgres, set
POPOTO_BACKEND=postgres and POPOTO_POSTGRES_URL in Claude Code's environment; see
On Postgres.
One turn becomes one record, verbatim. Issue #489 measured sentence-splitting extraction at 0.2078 judged accuracy against 0.3636 for raw ingestion on the same slice, so raw is the default here.
Nothing leaves your machine. There is no API key because there is nothing to authenticate to.
Inspect it directly:
Upgrading to 1.9.0+ on an existing corpus can delete records
DefaultMemory caps itself at 1000 records per agent_id. If your
corpus is already over that when you upgrade, the first save afterward
deletes the entire excess at once, synchronously, inside that one
save — not a gradual trim, and no tombstone. Check the record count
with popoto-memory doctor before upgrading. Set
POPOTO_DEFAULT_MEMORY_MAX_RECORDS=0 (no config edit needed) to disable
eviction first if you want to size or stage the cleanup yourself. See
Corpus growth for
the full mechanics.
Latency¶
Measured on an Apple M-series laptop, Python 3.12.13, redis 8.6.2 on
localhost, 50 records in scope, 25 subprocess invocations of the read hook:
p50 193 ms, p95 200 ms, max 211 ms, against a 400 ms p95 budget that
tests/test_integrations_latency.py enforces.
Nearly all of that is Python interpreter startup. The Redis work itself is 1-2 ms.
The MCP tools¶
The plugin also registers a stdio MCP server, or add it yourself:
memory_search, memory_save, memory_feedback, memory_status. These
are for deliberate use: searching for something specific mid-task, saving a
fact the turn text would not preserve, marking a memory wrong. Recall and
capture do not depend on them, and Claude never has to call one for memory
to work.
Tuning¶
Every setting is an environment variable and every one is optional. The full table is in Harness Integration. The three worth knowing:
export POPOTO_MEMORY_AGENT_ID=my-project # share one corpus across directories
export POPOTO_MEMORY_MAX_ITEMS=10 # default 5
export POPOTO_MEMORY_ENABLED=0 # kill switch, no config edit
export POPOTO_DEFAULT_MEMORY_MAX_RECORDS=0 # disable the per-agent eviction cap (1000 default)
Hooks inherit your shell environment, so exporting these in your shell profile is enough.
When it is not working¶
What doctor says |
What it means |
|---|---|
redis UNREACHABLE |
No server, or the wrong POPOTO_MEMORY_URL. It prints the fix. |
postgres UNREACHABLE / UNSUPPORTED |
On POPOTO_BACKEND=postgres: no server at POPOTO_POSTGRES_URL, or one older than PostgreSQL 18. |
last activity never |
The hooks are not firing. Check that popoto-memory is on the PATH your harness sees, and that you restarted Claude Code. |
retrieval composite -- QUERY-BLIND |
Ranking is ignoring the prompt text. See Query-Blind Retrieval. |
FAILURES assemble=N |
Something is raising on the read path. The last five log lines print underneath. |
records 0 after several turns |
Capture is not running. Check the Stop hook specifically; UserPromptSubmit can work while Stop does not. |
Test a hook by hand, which is exactly what Claude Code does:
echo '{"hook_event_name":"UserPromptSubmit","session_id":"t","prompt":"how do deploys work?"}' \
| popoto-memory hook
You should get one line of JSON, or nothing at all if no memory matched. Never a traceback, and never a non-zero exit.
Uninstall¶
Your memories stay in your Redis (or Postgres schema) until you delete them. On Redis:
Verified against¶
claude 2.1.220, on macOS, 2026-08-07. The UserPromptSubmit and Stop
payloads in tests/fixtures/harness_payloads/claude_code_*.json were
captured from a live run of that version, and each file records the command
that reproduces the capture.