Skip to content
Conduit

skill

claude-code-memory-hooks

Long-term memory for Claude Code built from hooks

Oleg67/claude-code-memory-hookscommunitylistedunreviewed

Set up

Skills are installed by placing their folder where your agent loads skills.

The download comes from github.com/Oleg67/claude-code-memory-hooks itself, not from our server, so what you get is exactly what its author published.

Install instructions come from the README

Skills do not use an MCP config. Follow the steps in the README below.

README

synced Oct 22, 2026

memory_tools

Long-term memory for Claude Code, built from hooks and plain local files: five memory types, confidence, Ebbinghaus decay, crystallization, a promotion gate for inferred memories, and its own file + SQL + vector + graph storage. No service; stdlib only except the embedding venv. For a portable version that works with any MCP client, see halflife-mcp.

FileRoleRuns from
memory_trace.pyAppends a redacted raw timeline (Trace)UserPromptSubmit, PostToolUse, PostToolUseFailure hooks
memory_distill.pyOne headless haiku call per non-trivial session → Episodic + candidates, through the gateSessionEnd (and --catch-up at SessionStart), detached
memory_recall.pyPer-prompt hybrid retrieval (BM25 + vector + graph, RRF): injects the ≤3 best memoriesUserPromptSubmit
memory_store.pySQL + vector + graph index of the memory files, in one SQLite fileimported by recall, distill, index
memory_index.pyCLI: sync, stats, query, graph, backfill, rebuildby hand
memory_maintain.pyDecay, supersession, archive, score-ordered MEMORY.md, trace retention, grouped session-start summarySessionStart --inject
skill_maintain.pyExperience-leveled skills report + memory → skill suggestionsSessionStart --inject

All hooks are global (~/.claude/settings.json), exit 0 on any error and log to maintain.log. hooks.json is the hooks block to merge into ~/.claude/settings.json; keep the two in sync. skills/<name>/SKILL.md holds the agent-side memory skill and the skills promoted from memories (plan-graph, public-core-release, script-timeouts), each installed as a symlink: ln -s ~/.claude/memory_tools/skills/<name>/SKILL.md ~/.claude/skills/<name>/SKILL.md. The SessionStart hook has no matcher, so it also fires after context compaction (source: compact) and re-injects the summary; a PreCompact hook could not, as its stdout is not added to context.

Install

Needs Python 3, a POSIX shell, and the claude CLI on PATH: the distiller runs claude -p --model haiku once per non-trivial session, using your Claude Code login.

  1. Clone this repository to ~/.claude/memory_tools; hooks.json assumes that location.
  2. Optional, for vector search and skill matching: create ~/.claude/memory_tools/.venv and install fastembed and numpy into it. Without it, recall uses keyword and graph search.
  3. Merge the hooks block of hooks.json into ~/.claude/settings.json.
  4. Link the skills as shown above.

The five types

TypeWhereTier / half-lifeWritten by
Trace<memdir>/_traces/<session_id>.jsonlnot a memory; deleted after 30 daysmemory_trace.py (every prompt and tool call)
Episodic<memdir>/episode_<date>_<sid8>.mdepisodic, 14 d, conf 0.5distiller at session end (or the agent)
Preference~/.claude/memory_global/ (cross-project) or <memdir>preference, 180 d, conf 0.7distiller candidate (or the agent)
Procedural<memdir>procedural, 365 d, conf 0.85distiller candidate (or the agent)
Fact / long-term<memdir>semantic (aliases fact, long_term), 180 d, conf 0.7distiller candidate (or the agent)

Plus the existing working (1 d), idea (age-immune) and rule (90 d, re-verify) tiers. <memdir> = dirname(transcript_path)/memory, i.e. ~/.claude/projects/<slug>/memory.

Trace

One line per event: {ts, event, tool, input_summary ≤200ch, ok, error?} or {ts, event, prompt ≤300ch}. The input summary is a pointer (command, file path, URL, pattern), never file contents or tool output. Secrets are redacted before writing: Bearer/Basic credentials, user:pass@ in URLs, JWTs, sk-…/ghp_…/glpat-…/xox…/AKIA keys, *key*/*token*/*secret*/*password*=value, and any .env-style UPPER_CASE=value. Never injected into context.

Distiller and promotion gate

At SessionEnd, memory_distill.py --detach forks into the background and:

  1. Skips trivial sessions (< 3 prompts and < 5 tool calls); writes _traces/<sid>.done.
  2. Builds a prompt from the trimmed transcript (user/assistant text only, head 4k + tail 26k chars), a trace summary, and the existing memory index (names + descriptions).
  3. Runs claude -p --model haiku --output-format json --tools "" --strict-mcp-config --no-session-persistence in /tmp/claude-memory-distill with CLAUDE_MEMORY_DISTILL=1 (every hook script exits at once when that is set: recursion guard) and auto-memory disabled.
  4. Writes the episodic memory (active) and applies up to 3 candidates:
    • matches an existing memory (same tier, description-token Jaccard ≥ 0.6) from an earlier session → reinforce: confidence +0.1 (cap 1), reinforced +1, last_reinforced = today, provisional → active;
    • no match → new file, status: provisional, confidence: 0.3;
    • contradicts: <file> is recorded on the new provisional memory; the old one is left alone until the new one is corroborated (see Supersession).
  5. Re-indexes MEMORY.md; records status, actions and the raw LLM result in .done.

Provisional memories are kept out of MEMORY.md and the injected summary until corroborated; if never corroborated they fade and archive like anything else. An LLM failure is retried once (next catch-up), then marked failed. A resumed session is re-distilled when the new activity is itself non-trivial (same episode file, updated).

--catch-up (SessionStart) distills traces idle > 1 h without a .done (sessions killed before SessionEnd), skipping the live session. MEMORY_DISTILL_LLM_CMD replaces the LLM command (tests).

Supersession

When an active memory carries contradicts: <file> (a corroborated distiller candidate, or one the agent wrote), memory_maintain.py marks <file> status: superseded, superseded_by: <new>, superseded_at: <date> and moves it to _archive/ with its body intact. A provisional contradiction never retires anything. The lookup is within the same memory dir; status: superseded set by hand is archived the same way. Superseded memories are never indexed, injected or recalled.

Storage: files + SQL + vector + graph

The .md files are the source of truth: Claude Code reads them, and you can edit them. Each memory can have a graph sidecar, <memdir>/_graph/<file>.json: {"entities": [{name, type}], "relations": [{src, rel, dst}]}.

data/memory.db (SQLite, WAL) is a derived index over all memory dirs. You can delete it at any time; memory_index.py rebuild recreates it from the files.

TableHolds
memoriesone row per file: frontmatter fields, body, content hash (incremental sync)
vectors384-d float32 embedding (fastembed BAAI/bge-small-en-v1.5, local, offline after first download; cosine in numpy)
entities / mentions / edgesthe knowledge graph: typed entities, which memory names them, typed relations with the memory that asserted them
linksmemory → memory (contradicts, superseded_by)

Entity types: file, repo, tool, library, service, person, concept, ticket, command. Relations: uses, depends_on, part_of, configures, fixed, caused, contradicts, supersedes, related_to.

  • Graph extraction: the session-end distiller returns a graph for each episode and candidate, which is saved as the sidecar. This costs no extra LLM call. For memories without a sidecar, run memory_index.py backfill: one haiku call per 6 memories.
  • Sync: hash-based, so only changed files (or sidecars) are re-indexed and re-embedded. Recall syncs the current project and global dirs on every prompt. The distiller syncs after writing.
  • Python: fastembed and numpy live in memory_tools/.venv (uv, Python 3.12). The recall and distill hooks run with that interpreter, falling back to system python3. Without it, the index keeps SQL + graph and recall drops the vector signal. Trace and maintain stay stdlib-only.

Inspect it:

P=~/.claude/memory_tools/.venv/bin/python
$P ~/.claude/memory_tools/memory_index.py stats
$P ~/.claude/memory_tools/memory_index.py query "why do graph tests fail"
$P ~/.claude/memory_tools/memory_index.py graph postgres
sqlite3 ~/.claude/memory_tools/data/memory.db 'select tier, count(*) from memories group by 1'

Per-prompt recall (memory_recall.py)

On each prompt (≥ 2 content words; stopwords dropped, plural -s stripped), three ranked lists are built over the active project + global memories:

  • bm25: BM25(prompt, name + description + body). A memory must share ≥ 2 distinct prompt terms; for long prompts the bar is 15 % of them, capped at 4. When vectors exist, a keyword hit with cosine < BM25_VEC_FLOOR = 0.50 is dropped as noise.
  • vector: cosine to the prompt embedding, kept when ≥ VEC_MIN = 0.66. Calibrated on these memories: unrelated prompts score about 0.47, the right memory 0.68–0.80.
  • graph: memories naming an entity that appears in the prompt score 1.0 per entity; memories naming a 1-hop neighbour of it score 0.5. Hub neighbours (named in more than 3 memories, e.g. pytest, a repo name) are not expanded, since they would link everything.

They are fused with RRF (k = 60), multiplied by the memory score, cut at half the best hit, and up to TOP_K = 3 are printed with absolute paths and the signals that fired (e.g. [bm25+vector+graph:postgres]). Each memory is injected at most once per session (_traces/<sid>.recall). If the index fails, recall falls back to bm25 alone. Cost is about 0.6 s per prompt, mostly loading the embedding model; no LLM.

Maintenance (memory_maintain.py)

python3 memory_maintain.py [MEMORY_DIR] [--global DIR]             # maintain + score table
python3 memory_maintain.py [MEMORY_DIR] [--global DIR] --dry-run   # report only
python3 memory_maintain.py [MEMORY_DIR] [--global DIR] --inject    # maintain + summary

Without MEMORY_DIR, --inject takes it from the hook's transcript_path on stdin, else from cwd. Read-only on memory bodies. It:

  • scores confidence × exp(−days_since(last_reinforced) / half_life[tier]);
  • supersedes memories contradicted by an active memory (see Supersession);
  • archives score < 0.10, unpinned, non-idea/rule memories to _archive/ (move, never delete);
  • rewrites MEMORY.md by score, preserving hand-written hooks, excluding provisional;
  • deletes _traces/* older than 30 days (raw logs, not memories);
  • --inject prints a block grouped Preferences / Procedural / Facts / Recent episodes (global preferences merged, marked global), SUMMARY_TOP_K split round-robin across sections, plus _trace: last session: N tool calls · distilled ✓|pending_ and a footer (re-verify, promote, near-duplicates, archived, provisional awaiting corroboration).

Memory → skill suggestions (skill_maintain.py)

A rule that lives only in memory works when recall happens to inject it; a rule in a skill loads whenever that skill does. At session start, skill_maintain.py --inject prints one line when memories are ready to become skill rules:

  • Eligible: tier preference or procedural, not provisional or superseded, confidence ≥ SUGGEST_MIN_CONFIDENCE = 0.7 or reinforced at least once, and no implemented_in.

  • Target: only the user's own skills, i.e. ~/.claude/skills/* not listed in .antigravity-install-manifest.json (a reinstall would overwrite edits to those). The memory description is embedded against <skill>: <description>; cosine ≥ SKILL_MATCH_MIN = 0.70 → update <skill>, else new skill. Calibrated on the current memories: the real match scored 0.75, unrelated pairs ≤ 0.66. Without fastembed the target is reported as unchecked.

  • Cache: data/skill_suggest_cache.json, keyed on the memory description and all own skills' descriptions, so the model (≈1 s) loads only when one of them changes.

  • Apply: agent work, only after the user accepts (see the memory skill). Then set implemented_in: <skill> on the memory, or implemented_in: declined, and the suggestion stops. The memory itself stays and is still recalled.

    P=~/.claude/memory_tools/.venv/bin/python $P ~/.claude/memory_tools/skill_maintain.py --global ~/.claude/memory_global # full report

Frontmatter (under metadata:)

type, tier, confidence, reinforced, last_reinforced (decay anchor, not mtime), status (provisional|active|superseded), implemented_in (skill name or declined), contradicts, superseded_by, superseded_at, revisit (pins), created, originSessionId.

Tests

python3 -m unittest discover ~/.claude/memory_tools/tests -v

Tunables live at the top of each script. Agent-side workflow (inspect, search, remember, reinforce, supersede, forget): the memory skill, ~/.claude/skills/memory/SKILL.md.

License

Apache License 2.0; see the LICENSE file.

↑↓ navigate openesc close