Skip to content

Using claude-memory after install

Day-to-day operation on the laptop once INSTALL.md is done. Server-side operations (backups, restore, upgrades) are in ../DEPLOY.md.

Check that it works

Open a new Claude Code session (hooks and MCP servers are read at session start) in any repo and ask something you know is stored, e.g. "which branch do I open the PR into" from billing-service. The prompt gets a short "Possible relevant memories" block with title / repo / id lines. No block means nothing scored above MEMORY_HOOK_SIM_THRESHOLD (0.50) — or something is down; check with:

claude mcp list | grep claude-memory          # ✔ Connected
ollama ps                                     # bge-m3 … UNTIL Forever (after first use)
echo '{"prompt":"which branch do I open the PR into","cwd":"'"$PWD"'"}' \
  | ~/.claude/hooks/claude-memory/user-prompt-submit.sh

The hook is silent on any error by design (it must never block a prompt). For details: MEMORY_DEBUG=1 claude-memory hook < input.json.

What leaves your machine

  • Embeddings are local. Text is embedded by your own Ollama instance; it is never sent elsewhere for embedding.
  • Extraction sends text to Anthropic. Session transcripts (at session end) and PR text (during PR ingest) are sent to Anthropic through your own claude -p --model haiku call, using your own Claude Code login. Before sending, the text is run through the built-in secret scrubber (tokens, API keys, JWTs, bearer headers, connection strings with passwords, private keys, password=-style assignments).
  • The scrub is pattern-based and best-effort, not a guarantee. A secret in an unusual shape will not be caught. Do not rely on it for highly sensitive sessions; disable the hook for those.
  • Nothing is sent to any other third party.
  • PR ingest calls the provider CLIs (az, gh, glab) on your machine with your own credentials, to read PRs from your own repositories.

What gets captured, and how

Path When Needs
Hook cards (read) every prompt hooks in ~/.claude/settings.json
Session extraction on SessionEnd, sessions with file edits or > 20 messages hooks; uses claude -p (haiku)
/remember when you ask skill in ~/.claude/skills/
Inline capture by Claude during work, without asking the rules from claude-md-snippet.md pasted into the shared CLAUDE.md — without them Claude does not store or search on its own
PR ingest manual or launchd az / gh / glab logged in, MEMORY_PR_INGEST_REPOS

/memory-digest summarizes what memory holds for a repo.

PR ingest (run manually)

Each PR costs two haiku calls; the first run looks back 30 days (MEMORY_PR_INGEST_LOOKBACK). Run it when you have quota to spare:

claude-memory ingest-pr --dry-run          # what would be ingested, no haiku, no writes
claude-memory ingest-pr                    # real run; cursors in ~/.local/state/claude-memory

# one repo only
MEMORY_PR_INGEST_REPOS=$HOME/work/acme/billing-service claude-memory ingest-pr --dry-run

A linked git worktree is not ingested separately when its main checkout is also among the discovered repos (same origin, same PRs); with no main checkout in the list it is ingested as usual.

Azure DevOps (az), GitHub (gh) and GitLab (glab) repos are ingested; the provider comes from the repo's origin. Log in once per CLI (gh auth login, glab auth login); claude-memory doctor shows the tools.gh / tools.glab rows. Re-running after a failure is safe: a PR whose haiku extraction fails (call error, error envelope, unparsable output) is not passed by the cursor. The cursor stays at the last PR processed before it, that repo's batch stops with a WARN, and the next run retries it. A PR that fails 3 runs in a row is skipped with an ERROR log naming it (so one bad PR cannot block a repo forever); its knowledge is not extracted.

Cursors live in ~/.local/state/claude-memory/pr-cursors/<provider>__<repo>.json:

cat ~/.local/state/claude-memory/pr-cursors/*.json   # since, failed_pr, failures
# re-ingest from a date: edit "since" (and delete "failed_pr"/"failures")
# re-ingest a repo from the lookback window: delete its file
rm ~/.local/state/claude-memory/pr-cursors/azuredevops__billing-service.json

failed_pr/failures count consecutive failed runs of the PR currently blocking that repo.

  • GitHub is github.com only (no GitHub Enterprise).
  • GitLab is auto-detected for gitlab.com only. A self-hosted GitLab needs an explicit opt-in on its namespace (see below). The GitLab adapter is not verified against a live GitLab (tested with fixtures written from the API docs): start with --dry-run. The GitHub adapter is also tested with hand-written fixtures; live GitHub verification is a manual step (--dry-run, then one real run).
  • Only trusted authors are ingested (GitHub: OWNER/MEMBER/ COLLABORATOR; GitLab: Developer or higher), and bots are skipped. PR text from strangers on a public repo would otherwise become memory records that are injected into your sessions. Only their comments are kept, and PR text is scrubbed and capped before it reaches haiku.
  • The CLIs run with GH_TOKEN/GITHUB_TOKEN/GITLAB_TOKEN and similar variables removed from their environment: they use their own login (keychain), never a token from claude-memory's environment.
  • A first run on a busy repo can hit the page cap (1000 PRs updated since the cursor); the error names the cursor file: move its since forward.

Per-namespace opt-out or provider override in namespaces.yaml:

namespaces:
  - namespace: sandbox
    paths: ["~/src/sandbox/**"]
    pr_ingest: {enabled: false}        # skip this namespace's repos
  - namespace: corp
    paths: ["~/work/corp/**"]
    pr_ingest: {provider: gitlab}      # self-hosted GitLab (host from origin)

A bad pr_ingest never breaks namespace resolution: ingest-pr skips that namespace with a warning and doctor reports it on the namespaces row.

To schedule it daily instead, load the launchd job from INSTALL.md step 8.

Internal claude -p calls and CLAUDE_MEMORY_SUBPROCESS

Extraction (SessionEnd and ingest-pr) shells out to claude -p --model haiku --output-format json --no-session-persistence --strict-mcp-config --tools "": no saved session, no MCP servers, no tools. The subprocess runs with CLAUDE_MEMORY_SUBPROCESS=1, and claude-memory hook and claude-memory extract exit 0 immediately (hook prints nothing) when it is set. Without that guard your own hooks would inject memory hints into the extraction prompt and run extract on the call's own transcript. Your own scripts that call claude -p for something unrelated can set the same variable to keep claude-memory's hooks out of them.

Cleanup

Candidates nobody used or re-saw for 180 days (MEMORY_CANDIDATE_TTL) are deleted; active and deprecated records are never touched.

claude-memory cleanup

It prints the number of deleted candidates, then the number of usage events pruned (events older than 365 days), and a third line spool: N draining, M failed files left only when spool files remain (see Usage stats). It also moves any pending hook events into the database and removes per-session staleness caches older than 7 days.

Usage stats

The hook, memory_feedback and every write record small usage events (record ids, enums and numbers only, never text) in the events table. The hook only appends them to ~/.local/state/claude-memory/events/spool.jsonl; serve, extract, ingest-pr and cleanup move the spool into Postgres.

claude-memory stats [--since 30d] [--json]   # window: Nd or a Go duration, e.g. 36h

One block for all namespaces and one per namespace: cards injected and distinct records, the precision proxy (records marked useful within 2 h of a card / distinct records injected) next to the same ratio without the 2 h limit, feedback by outcome, records created by source and the current inventory, candidate-to-active promotion rate (imported candidates are left out and shown on their own import promotion line), stale-flag rate and check coverage (cards whose staleness could be checked), superseded/deprecated/TTL-deleted counts. A ratio with no data prints n/a. The last line, N events still in the spool, tells you the report may lag. The proxy depends on Claude calling memory_feedback(useful); compare trends, not absolute numbers.

Reliability counters

Memory fails softly, so stats also counts the calls themselves: one event per memory_search / memory_store call of the MCP server and one per hook run (searches only). Events hold an outcome (ok, degraded for an embedding-down fallback, error, and for stores added, updated, superseded, noop, needs_judgment) and, for errors, a class: db_unavailable, embedding_unavailable, invalid_request, timeout or internal. Never a query, title, content or error text. serve runs under a random session id, so the latest serve session line shows the most recent session across all namespaces (not necessarily the current one): its searches, store attempts and failures. Each block prints searches (mcp), searches (hook), store attempts, failures, failure rate and db-down hours (UTC clock hours with at least one db_unavailable event).

  • Events go through the file spool, so they are recorded while Postgres is down. stats adds spool events the table does not hold yet (reliability events counted from the spool), so the report is current without a drain.
  • The hook now appends on every prompt, sharing the 10 MB spool cap with card events. Past the cap events are dropped and not counted: stats then prints spool is full: new events are being dropped and not counted. serve start, cleanup, extract and ingest-pr drain the spool.
  • Mixed versions: an old binary draining the spool skips the new event types and deletes the file (logged at Debug only), so those events are lost. This matters only while old and new binaries run side by side.
  • A timeout class can hide an unreachable database in serve: a connect that hits the deadline surfaces as a plain deadline error. The hook records any service-build failure as db_unavailable.
  • stats reads the table counts, then scans the spool. A drain between the two can briefly undercount, never double count.
  • Writes by extract, ingest-pr and import are not counted.
  • An outage that spans a whole session is invisible to the MCP counters (serve does not start when Postgres is down); only the hook's db_unavailable events show it.

Managing records from the terminal

Look at and fix memory without a Claude session. Every command works on one namespace: the current directory's, or --namespace NAME (which always wins). Writes go through the same path as the MCP tools (scrubbing, re-embedding, events), so edit needs Ollama; ls, show, rm and promote do not.

claude-memory ls [--status S] [--kind K] [--repo R] [--limit N]
claude-memory show ID
claude-memory rm ID [--reason TEXT]            # deprecates (reversible)
claude-memory rm ID --hard [--yes]             # deletes the row; asks y/N first
claude-memory edit ID                          # opens $VISUAL, else $EDITOR, else vi
claude-memory promote ID                       # candidate -> active
claude-memory review                           # weekly pass over candidates
  • ids are the full UUID or the 8-hex short id that ls prints. A short id is matched only inside the namespace; a global record needs its full UUID. rm, edit, promote and review refuse a record of another namespace (including global): pass --namespace global for those.
  • ls shows candidate and active records, newest first (50 by default, --limit 0 = all); --status deprecated shows the retired ones.
  • show prints every field, the content and a stale line: stale: N commits, stale, fresh or unchecked (unchecked unless the record's repo is the current directory's git checkout and it has a baseline commit and files).
  • rm --hard asks delete <id> "<title>" permanently? [y/N]; EOF and an empty answer mean no, --yes skips the question. It refuses a record that another record's superseded_by points at; deprecate it instead.
  • edit shows title:, tags:, files: (comma-separated), a --- line and the content. Nothing changed means nothing written; an empty tags:/files: line cannot clear them. If the editor fails or the file does not parse, nothing is written and the temp file is kept (its path is printed).
  • review walks candidates newest first. Per record it shows age, counts, the stale line, tags, files, the first 15 content lines and up to 3 similar records, then asks [a]pprove [e]dit [d]eprecate [x]delete [s]kip [v]iew [q]uit: a promotes, d asks a reason (Enter = rejected in review), x asks y/N, s or Enter skips, v prints the whole content, q or EOF stops. EOF at the reason or y/N prompt aborts that action. It prints a summary of the counts at the end. It reads plain lines, so it can be scripted.

Importing existing knowledge

One-off import of Claude Code auto-memory files and INSIGHTS.md logs as candidate records (source import, tags imported plus auto-memory or insights). Run --dry-run first: it needs no database, Ollama or env file and prints, per item, the kind, repo, the scrubbed title, the file count, an import-key prefix and would redact: yes|no, plus every skip with its reason (never file content). It does not evaluate dedup against existing records.

claude-memory import automem --dry-run [--projects-dir DIR] [--namespace NS]
claude-memory import automem            # real run: needs Postgres and Ollama
claude-memory import insights [PATH...] [--dry-run] [--namespace NS]
  • Namespace. Imports go to --namespace, else the current directory's namespace from namespaces.yaml (MEMORY_NAMESPACE is ignored). With no mapping for the directory and no --namespace the command refuses (exit 2); global only with an explicit --namespace global. An item whose own directory maps to another namespace is skipped (other namespace (X)).
  • automem reads <projects-dir>/*/memory/*.md (default ~/.claude/projects), regular files only. MEMORY.md, CLAUDE.md, files without frontmatter or description, and type: user are skipped; feedback becomes convention, project decision, reference pattern. The repo is the git toplevel's name of the decoded project directory, else *.
  • insights reads dated bullets (- YYYY-MM-DD — text) under What Works, Codebase Patterns (pattern) and What Doesn't Work, Recurring Errors & Fixes, Tool & Library Notes (gotcha); other sections (including Open Questions, Session Notes) are skipped. A PATH is a file or a directory (default: the git toplevel of the current directory), walked without following symlinks. Backticked file references that name an existing file inside the checkout become the record's files (no commit baseline).
  • Re-runs are no-ops. Each item has an import key (a hash of its location and text) stored on the record, so a second run prints skipped (already imported) even after you edited or deprecated the imported record. A new item whose similarity to an existing record is 0.85 or more is skipped as duplicate of <id>; all writes go through the normal path (scrubbing, embedding, events). A bad item (for example oversized) is skipped with a reason; a database or Ollama error stops the run, and running it again continues where it stopped.
  • TTL. Imported candidates are ordinary candidates: cleanup deletes the unreviewed ones after the candidate TTL, and a later import re-creates them (the key is deleted with the row). The run ends with N candidates awaitclaude-memory review; unreviewed candidates are deleted after <TTL> days (MEMORY_CANDIDATE_TTL) (default 180): review them soon after importing.
  • Live before review. Imported candidates are searchable and are injected into sessions (marked unverified) immediately, before you review them, and the database is shared. Run --dry-run first and review soon.
  • Files that are not valid UTF-8 or contain NUL bytes, symlinked memory directories and undated or malformed INSIGHTS bullets are skipped with a reason.

Seeding and correcting records

claude-memory seed --file seed/facts.yaml --dry-run   # parse only
claude-memory seed --file seed/facts.yaml             # store via the normal write path

Re-running seed is safe: near-identical facts resolve to NOOP. A fact whose closest existing record scores 0.65–0.85 is reported as ASK (needs manual judgment, not stored) — store it from a Claude session with memory_store + decision: {"action": "ADD"} (or UPDATE with target_id). Fix an existing record with memory_update, retire it with memory_deprecate.

Config reference

~/.config/claude-memory/env (must be chmod 600, otherwise it is refused):

Variable Default Meaning
MEMORY_PG_DSN — (required) postgresql://claude_memory:<pw>@<server-tailscale-ip>:5432/claude_memory
MEMORY_OLLAMA_URL http://127.0.0.1:11434 local Ollama
MEMORY_OLLAMA_MODEL bge-m3 embedding model
MEMORY_EMBED_MAX_TOKENS 2048 embedding input cap (Ollama num_batch)
MEMORY_HOOK_SIM_THRESHOLD 0.50 min cosine similarity for a hook card
MEMORY_STORE_SIM_ASK / _UPDATE 0.65 / 0.85 inline dedup bands
MEMORY_HOOK_TIMEOUT 800ms hard hook timeout
MEMORY_EXTRACT_MIN_MESSAGES 20 session extraction gate
MEMORY_CANDIDATE_TTL 180d cleanup age for unused candidates
MEMORY_PR_INGEST_REPOS — comma-separated repo dirs or roots
MEMORY_PR_INGEST_LOOKBACK 30d first-run window per repo

Troubleshooting

Symptom Check
No cards ever claude mcp list; Tailscale up; psql to the DSN works; prompt really matches something (memory_search from a session)
First prompt after reboot slow Ollama loads the model on first use; warm it: curl -s localhost:11434/api/embed -d '{"model":"bge-m3","input":"warmup","keep_alive":-1}'
env file … permissions error chmod 600 ~/.config/claude-memory/env
Away from home network Hook stays silent, memory_store returns an "unavailable" error (no offline queue yet)
Undo the hooks restore ~/.claude/settings.json.bak-claude-memory or remove the hooks entries; see INSTALL.md → Uninstall