Skip to content

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

pip install 'popoto[mcp]'
popoto-memory doctor

doctor must print redis reachable and retrieval lexical (query-sensitive) before you go further. If Redis is not running:

redis-server        # or: valkey-server

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

claude plugin marketplace add tomcounsell/popoto
claude plugin install popoto-memory@popoto

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:

popoto-memory doctor

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:

redis-cli --scan --pattern 'DefaultMemory:*' | head

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:

{ "mcpServers": { "popoto-memory": { "command": "popoto-memory", "args": ["mcp"] } } }

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

popoto-memory doctor
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

claude plugin uninstall popoto-memory@popoto   # or delete the settings block
pip uninstall popoto

Your memories stay in your Redis (or Postgres schema) until you delete them. On Redis:

redis-cli --scan --pattern 'DefaultMemory:*' | xargs redis-cli del

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.