Custom Claude Memory System

Persistentní paměť pro Claude Code přes SQLite + Chroma vector DB + hooks. Kontextová kontinuita napříč sessions.

SQLiteChromaBash hooksClaude Code

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;
SQL

Output 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-store

MCP 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:

ToolPouž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:

  1. Vybere stale memories (importance ≤ 2, last_accessed_at > 30 dní)
  2. Komprese - pošle bulk Claude promptu "summarize těchto 50 memories do 5", uloží sumarizované memories s odkazem na originály
  3. Archive original do ~/.claude-mem/archives/<year>-<month>.json.gz
  4. 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

MetrikaHodnota
Persisted memories1 240
Sessions s memory hit89 %
Avg memory recall na session4.2
Storage footprint47 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.