Local-first
Everything in ~/.ccrecall/ is local SQLite. No cloud, no telemetry, no outbound HTTP.
DEPS: SQLite (system)
SIZE: ~570 KB
NETWORK: none
Week one you picked SQLite over MongoDB, to avoid running one more service. Week two you fixed an ImageMagick edge case. Week three you settled on a file naming convention.
Then you open a new session — and Claude remembers none of it.
All of it is still sitting in ~/.claude/projects/ as JSONL — nothing reads it back. ccRecall does: after a session ends it distills the conversation into structured memories, and the next session starts with up to 5 of them. Local SQLite, no cloud, Apache-2.0.
npm install -g @tznthou/ccrecall
[ccRecall memory recall]
- On macOS use cwebp -q 85 for PNG→webp, not sips — sips fails silently on CJK filenames, prints success but writes nothing…
- Tailwind CLI projects: after editing src/input.css you must run npm run build, because dist/output.css is committed…
- Deploy platforms detect runtime from version files, so .node-version and friends must be committed with an upper bound…
(608 memories available — use recall_query to search more)
A memory store is long-tail. Load all of it and the first thing a session does is eat your context window — mostly with things irrelevant to today's work.
So it ships in two parts: SessionStart injects at most 5 entries within <300 tokens; everything else stays in the store until Claude decides mid-conversation that it needs more and calls recall_query itself.
// Corrected the "auto-injects" overstatement in 2026-05, and again in 2026-07 — injection is real, it just has a budget.
// INDEX(daemon watches .jsonl) → DISTILL(Haiku, optional) → INJECT(SessionStart ≤300 tokens) → RECALL(MCP on-demand)
Everything in ~/.ccrecall/ is local SQLite. No cloud, no telemetry, no outbound HTTP.
DEPS: SQLite (system)
SIZE: ~570 KB
NETWORK: none
Native integration with Claude Code lifecycle hooks (SessionStart / SessionEnd) plus the MCP server standard protocol.
HOOKS: SessionStart + End
MCP: v1 protocol
SETUP: install-hooks
Source code public and auditable. A simple SQLite + FTS5 + Node stack — no black-box vector DB dependencies.
LICENSE: Apache-2.0
REPO: tznthou/ccRecall
AUDIT: public
npm install / install-daemon / mcp add / install-hooks gets you indexing, startup injection, and manual recall_save. Memories still won't accumulate on their own — that's step 5: an optional wrapper that lets Haiku read the transcript after a session ends and distill 0–5 entries into the store, for about $0.001 a run.
One npm command installs ccmem (daemon) and ccmem-mcp (MCP server). Three more setup commands follow — daemon, MCP, hooks — plus one optional distillation wrapper. Full tutorial →
CMD: npm i -g @tznthou/ccrecall
BIN: ccmem + ccmem-mcp
NEXT: 3 steps + 1 optional
A chokidar watcher tails ~/.claude/projects/, so a new session lands in the index within seconds; the SessionEnd hook exists to confirm nothing slipped through. What actually turns conversations into memories is the optional distillation wrapper — after a session ends, Haiku reads the transcript and distills 0–5 entries (~$0.001/session). Cross-project recall since v0.4.1: when two projects share a topic, high-confidence memories surface across the boundary.
HOOK: SessionEnd (index check)
DISTILL: Haiku via wrapper (optional)
STORE: ~/.ccrecall/ccrecall.db
INDEX: FTS5 full-text
On the next session, the SessionStart hook injects up to 5 memories within a 300-token budget. Selection runs in three tiers: cold memories get a rotating slot first (since v0.5.5 anything injected recently is excluded, so the same few entries stop hogging the budget), then recent high-confidence ones, then FTS to fill any gap. Mid-conversation, Claude calls MCP recall_query / recall_context on its own judgement.
HOOK: SessionStart
MCP: recall_query / recall_context / recall_save
BUDGET: ≤5 rows · ≤300 tokens
SELECT: cold-rotate → confidence → FTS
// INVARIANTS: LOCAL-FIRST · NO-CLOUD · NO-TELEMETRY · USER-OWNS-DATA
The v0.3.0 design had two tiers: anything the machine captured landed in a low-trust session_journal, and only entries you reviewed and promoted by hand graduated to the memories table the AI can actually search. The reasoning held up on paper — a rule-based scorer mis-captures, and noise in a memory store is worse than no memory at all.
0
PROMOTIONS · EVER
177
STUCK IN QUEUE
Two months of real use returned those two numbers. Not one entry ever graduated. That is not a gate — that is a dead-letter queue.
So v0.5.0 cut the whole thing: the journal, the scorer feeding it, eight endpoints nobody called, and a bookkeeping table eating 65% of the database. The path that was actually doing the work turned out to be the other one — Haiku distills the session after it ends, writes keyed memories, and the next session starts with them.
// A design doc can argue for a gate. Only the data can tell you nobody walks through it.
The first question people ask is what a memory is actually stored as. Two layers: the container is a single .db file, the content is plain text. The drawer has no idea what is written on the note — it's just a TEXT column.
**Rule**: On macOS use cwebp -q 85 for
PNG→webp, not sips.
**Why**: sips fails silently on CJK filenames
— stdout reports success, nothing is written,
no stderr.
**How to apply**:
1. brew install webp
2. cwebp -q 85 input.png -o output.webp
// Bold, lists, code blocks are all markdown —
// SQLite stores them as ordinary text.
Obsidian
One .md file per memory, scattered across folders
Notion
Cloud database plus a block structure
ccRecall
One SQLite file, one row per memory
The cost: you can't open memories in Obsidian. What you get back is instant full-text search, confidence and access counts queryable in SQL, and a backup that's one file copy.
Topic extraction used to recognise Latin word boundaries only, which meant an entire Chinese, Japanese, or Korean session produced exactly zero topics — CJK users silently lost cross-project surfacing and metacognition queries. v0.5.3 brought CJK into the tokeniser (\p{Script=Han} plus 32 stopwords and particle splitting).
npm install is just step one — daemon, MCP, and hooks each need a command before it runs at all, plus one optional wrapper before memories start accumulating.
[ → READ FULL TUTORIAL ]