Custom Claude Memory System
Persistentní paměť pro Claude Code přes SQLite + Chroma vector DB + hooks. Kontextová kontinuita napříč sessions.
Brief
Claude Code má per-session memory. Když zavřeš okno terminálu, agent zapomíná všechno: jaké preference máš, jaký monorepo layout používáš, proč jsi tři dny zpátky odmítl konkrétní knihovnu, jaké production incidenty jsme řešili minulý měsíc. Pro one-off tasks to nevadí - open file, fix bug, close. Pro dlouhodobý vývoj (agentic workflows, vícetýdenní projekty) je to denní produktivní krvácení: každou novou session strávím prvních 10 minut tím, že agentovi vysvětluji, kde věci leží.
Postavil jsem si vlastní persistent memory layer. SQLite pro structured episodic memory (kdo / kdy / co / proč), Chroma vector DB pro semantic retrieval, bash hooks na začátek a konec session, MCP server pro průběžný query přes Claude tool use. Cíl: přijdu k novému claude promptu a do 5 sekund agent ví, na čem jsem včera skončil, jaké guidelines respektuju a kde najde relevantní kontext.
Architektura: SQLite + Chroma + hooks
Dva storage layers, každý dělá co umí:
- SQLite drží strukturované memories: id, project, type (
user_pref,feedback,project_fact,reference), body, embedding_id, created_at. Rychlé filtrování podle projektu, typu, dat. Plný text fallback přes FTS5. - Chroma drží embeddings téhož bodyu. Vector search dává semantic match ("oauth implementation" matchne i memories o "Clerk integration via middleware"), což keyword search nedělá.
~/.claude-mem/
├── memories.db ← SQLite (structured)
├── chroma/ ← Chroma collection (embeddings)
├── archives/ ← .gz JSON snapshoty starých sessions
└── hooks/
├── session-start.sh ← načte top 20 relevantních memories
└── session-end.sh ← extrahuje nové memories ze session transcriptu
Hooks fire přes Claude Code hooks API. SessionStart injektuje memory header do system promptu, Stop (end of agent loop) spustí archival job.
Schema
-- ~/.claude-mem/memories.db
CREATE TABLE episodic_memory (
id TEXT PRIMARY KEY, -- nanoid + project hash salt
project TEXT NOT NULL, -- 'prace-web', 'dokladbot', ...
type TEXT NOT NULL CHECK (type IN ('user_pref', 'feedback', 'project_fact', 'reference')),
body TEXT NOT NULL,
embedding_id TEXT NOT NULL, -- foreign id v Chroma collection
source_session TEXT NOT NULL, -- claude session id
importance INTEGER DEFAULT 1, -- 1-5, určuje retention
created_at INTEGER NOT NULL, -- ms epoch
last_accessed_at INTEGER, -- pro LRU eviction
access_count INTEGER DEFAULT 0
);
CREATE VIRTUAL TABLE episodic_memory_fts USING fts5(
body, content='episodic_memory', content_rowid='rowid'
);
CREATE INDEX idx_project_type ON episodic_memory(project, type);
CREATE INDEX idx_importance_recent ON episodic_memory(importance DESC, created_at DESC);type taxonomie:
user_pref- globální preference ("nikdy se neptej, vždy zvol druhou možnost", "preferuj Bun nad pnpm")feedback- explicitní reakce uživatele ("tohle bylo over-engineered", "lib X mě zklamala v projektu Y")project_fact- strukturní info o projektu ("monorepo s Turborepo, apps/web + apps/api", "Postgres na Neon")reference- dlouhodobé znalostní base ("DataForSEO API auth = basic auth, ne bearer")
importance 1–5 řídí retention. 1 = nejde do permanentního storage (jen single session), 5 = nikdy nemazat.
Hook lifecycle
session-start.sh:
#!/usr/bin/env bash
# ~/.claude-mem/hooks/session-start.sh
PROJECT=$(basename "$PWD")
DB=~/.claude-mem/memories.db
# top 20 memories pro tento projekt + globální user_prefs
sqlite3 -json "$DB" <<SQL
SELECT id, type, body, importance, created_at
FROM episodic_memory
WHERE project IN ('$PROJECT', '_global')
AND (type = 'user_pref' OR project = '$PROJECT')
ORDER BY importance DESC, last_accessed_at DESC
LIMIT 20;
SQLOutput je JSON, který Claude Code injektuje do prvního system message jako <memory-context> block.
session-end.sh má těžší práci. Bere transcript session, posílá ho přes Claude API se striktním promptem ("extract memories worth persisting, return JSON array, max 5"), a zapisuje do SQLite + Chroma:
#!/usr/bin/env bash
# ~/.claude-mem/hooks/session-end.sh
TRANSCRIPT=$1
PROJECT=$(basename "$PWD")
# 1. extract candidate memories
NEW_MEMORIES=$(claude-mem-extract --transcript "$TRANSCRIPT" --project "$PROJECT")
# 2. dedup proti existujícím (semantic similarity > 0.85 = duplicate)
FILTERED=$(echo "$NEW_MEMORIES" | claude-mem-dedup)
# 3. write to SQLite + Chroma
echo "$FILTERED" | claude-mem-storeMCP integrace
Sklad memories je k ničemu, když k němu Claude nemá runtime access. Postavil jsem MCP server (claude-mem-mcp), který exposuje 3 tooly:
| Tool | Použití |
|---|---|
chroma_query_documents(queries: string[]) | semantic search, top-k = 5 |
chroma_get_documents(ids: string[]) | retrieve full body podle id |
memory_store(type, body, importance) | explicitně uložit memory během session |
Globální CLAUDE.md má rule: "Před start tasku zavolej chroma_query_documents s názvem projektu + intent." Agent to dělá automaticky, dostane 5 nejrelevantnějších memories a jede.
Archival loop
Hot memories (poslední 30 dní, importance ≥ 3) zůstávají v SQLite + Chroma. Po 30 dnech nightly cron:
- Vybere stale memories (importance ≤ 2, last_accessed_at > 30 dní)
- Komprese - pošle bulk Claude promptu "summarize těchto 50 memories do 5", uloží sumarizované memories s odkazem na originály
- Archive original do
~/.claude-mem/archives/<year>-<month>.json.gz - Smaže originály z hot storage
Storage zůstává malý (~50 MB i po roce práce), ale historie je dohledatelná - když potřebuju vědět, jak jsem řešil X před 8 měsíci, archive search to najde.
Konkrétní metriky po 6 měsících
| Metrika | Hodnota |
|---|---|
| Persisted memories | 1 240 |
| Sessions s memory hit | 89 % |
| Avg memory recall na session | 4.2 |
| Storage footprint | 47 MB |
| Re-explanation overhead reduction | ~90 % (subjektivní, na 5 random sessions měřeno stopwatch) |
| AI cost/měsíc (extraction + dedup + summarize) | $1.80 |
90% redukce re-explained context znamená, že prvních 10 minut každé session, kdy jsem dřív mluvil "máme monorepo, packages/db obsahuje Prisma, apps/web je Next.js 15, ...", je teď 30 sekund "pokračujeme tam, kde jsme včera skončili."
Lessons
- Když paměť škodí. Over-eager recall taháje irrelevantní memories ("oh, řešili jsme tohle minulý rok, použij ten samý pattern") i když je situation jiná. Řeší to importance × recency × relevance score, ne pure semantic similarity. Memory s importance 1 a stáří 4 měsíce má skoro nulovou váhu, i když semanticky matchne.
- Salt pro hash IDs. SHA256 raw obsahu je zradné - same body napříč projekty produkuje stejný hash, vznikají falešné dedups. Salt =
${project}:${userId}:${body}řeší to. - Lokální SQLite > cloud DB. Cloud DB byly první nápad (sync mezi laptop + work machine), ale latence query při start session > 200 ms zruinuje UX. Lokální SQLite + opt-in rsync přes restic na encrypted S3 dělá totéž bez online dependence.
- Memory taxonomie je load-bearing. První verze mělala jen "memory" jako flat type. Po 200 entries jsem ztrácel přehled, co je preference vs fact vs feedback. 4 typy popsané výše drží order i ve velkém indexu.
- Hooks API je underrated. Claude Code hooks dávají moc bez fork ekosystému - chování se mění na úrovni mého stroje, nikdo jiný to nevidí, žádný PR do upstream.