Deterministic memory for Claude Code.
Long-term memory you can see, search, and trust.
No embeddings. No vector DB. No magic.
Just keyword scoring you can recompute by hand, and a folder of plain markdown you can grep.
A memory — plain markdown you can read, edit, or grep:
---
slug: split-shell-commands
type: correction
title: Split shell commands instead of chaining with &&
project: global
keywords: shell, bash, commands, chaining, split
created: 2026-06-14
updated: 2026-06-14
---
Prefer single atomic shell commands over compound commands chained
with && or ;. This lets permission allowlists match individual
commands reliably.What gets injected when you type "how should I run shell commands":
<recalled-memories>
<memory slug="split-shell-commands" type="correction" project="global">
Split shell commands instead of chaining with &&
keywords: shell, bash, commands, chaining, split
Prefer single atomic shell commands over compound commands
chained with && or ;. This lets permission allowlists match
individual commands reliably.
</memory>
</recalled-memories>What the log records — every decision, one greppable line:
2026-06-19T14:32:01Z RECALL session=s1 project=engram prompt_tokens=4 injected=1 slugs=[split-shell-commands:14] skipped=[]
git clone https://github.com/VictorBodnar/engram.git ~/.engram
python3 ~/.engram/scripts/manage.py installThat's it. Hooks are registered directly in ~/.claude/settings.json, pointing
at the scripts in place. Edits are live immediately — no rebuild, no sync, no
cache to invalidate.
Verify:
/engram status
store: ~/.claude/memory-store
memories: 0
by type: —
by project: —
state: 0 session file(s) (GC'd after 7d)
recent distiller activity:
(none logged yet)
There's no update step. Scripts are referenced directly — git pull is enough.
If you want to confirm everything is healthy:
python3 ~/.engram/scripts/manage.py verifyIf something broke (moved the repo, etc.):
python3 ~/.engram/scripts/manage.py repairpython3 ~/.engram/scripts/manage.py uninstallEngram uses Claude Code's native settings.json hooks — the simplest, most stable integration point. No plugin registry, no cache directories, no symlinks, no version-tagged paths.
~/.claude/settings.json (hooks section):
SessionStart → python3 /path/to/engram/scripts/session_start.py
UserPromptSubmit → python3 /path/to/engram/scripts/user_prompt.py
Stop → python3 /path/to/engram/scripts/stop_distill.py
PreCompact → python3 /path/to/engram/scripts/precompact.py
SessionEnd → python3 /path/to/engram/scripts/session_end.py
Why this model:
- Zero intermediaries — hooks point directly at scripts
- Edit any file, it's live instantly
- Survives Claude Code updates (settings.json is your config, not internal state)
- One
git pullto update, no sync/rebuild - Works everywhere: macOS, Linux, WSL
- Trivial to inspect:
cat ~/.claude/settings.json - Self-healing:
manage.py repairfixes anything
If you previously installed via /plugin install engram:
python3 scripts/manage.py migrateThis removes all plugin registry traces and switches to direct hooks. Your memory store is preserved.
flowchart LR
subgraph Capture ["CAPTURE — async, never blocks"]
direction TB
H["Hook fires<br/>(Stop · UserPromptSubmit ·<br/>PreCompact · SessionEnd)"]
D["Spawn detached distiller<br/>(claude -p haiku)"]
W["Read new transcript bytes<br/>↓<br/>Extract durable facts<br/>↓<br/>Write memories/*.md"]
H --> D --> W
end
Store[("memory-store/<br/>plain markdown")]
subgraph Recall ["RECALL — deterministic, in-hook"]
direction TB
SS["SessionStart → inject<br/>budgeted memory index"]
UPS["UserPromptSubmit →<br/>tokenize, score, inject top 3"]
end
W --> Store
Store --> SS
Store --> UPS
| Capture (write) | Recall (read) | |
|---|---|---|
| When | Stop, UserPromptSubmit, PreCompact, SessionEnd | SessionStart + every UserPromptSubmit |
| LLM? | Yes — detached Haiku, off the hot path | Never — integer arithmetic only |
| Speed | Hooks return {} in milliseconds |
In-hook, deterministic |
| What | Reads new transcript bytes, extracts durable facts | Scores every memory against your prompt |
| Engram | Native auto-memory | |
|---|---|---|
| Storage | Plain .md files you can read, edit, grep |
Opaque internal format |
| Recall | Deterministic integer scoring — reproducible | Model-driven, non-reproducible |
| Debugging | grep the log; recompute any score by hand |
No visibility into recall decisions |
| Capture | Hook → Haiku → markdown with frontmatter | Automatic, invisible |
| Control | /engram search, forget, clear, doctor |
Limited |
sequenceDiagram
participant S as Your Session
participant H as Hook (ms)
participant D as Distiller (background)
participant St as memory-store/
S->>H: Stop / UserPromptSubmit / etc.
H->>D: spawn detached (fire and forget)
H-->>S: return {} instantly
Note over D: Runs in background,<br/>your session never waits
D->>D: Read new transcript bytes<br/>(per-session byte cursor)
D->>D: claude -p haiku → JSON
D->>St: Write/update memories/*.md
D->>St: Rebuild INDEX.md
D->>St: Log CREATE/UPDATE
| Type | When | Example |
|---|---|---|
| correction | You state a preference or habit | "Don't chain shell commands with &&." |
| knowledge | A non-obvious codebase/environment fact | "Integration tests need LOCALSTACK=1." |
| state | A project decision or open item | "Chose SQS over Kafka; migration TODO open." |
flowchart LR
P["Your prompt"] --> T["Tokenize<br/>(lowercase, drop stopwords)"]
T --> SC["Score every memory<br/>(integer arithmetic)"]
SC --> G{"Score ≥ 3?"}
G -->|Yes| Top["Take top 3<br/>(not yet seen this session)"]
G -->|No| Skip["Skip"]
Top --> Inject["Inject into context"]
| Signal | Points | Example |
|---|---|---|
| Prompt word in memory keywords | +3 each | "shell" matches keyword shell |
| Prompt word in memory title | +2 each | "commands" in "Split shell commands…" |
| Memory belongs to current project | +2 | memory.project == cwd project |
| Memory type is correction | +1 | Standing preferences get a boost |
Gate: a memory must have at least one keyword or title hit. Project and type bonuses only boost a real hit — they can never qualify a memory alone.
Prompt: "the integration tests pass but nothing seems to run"
Project: payments-api
Tokens: [integration, tests, pass, run]
payments-localstack (knowledge · payments-api)
├── kw: tests +3
├── kw: integration +3
└── project match +2
─────────
Score: 8 ✓ INJECT
use-uv-run-scripts (correction · global)
├── title: "run" +2
└── type: correction +1
─────────
Score: 3 ✓ qualifies
payments-jwt-rotation (state · payments-api)
└── (no keyword/title overlap → gated)
─────────
Score: 0 ✗ skip
Session lifecycle:
SessionStart ─────────────────────────── Prompt 1 ── Prompt 2 ── Prompt 3 ...
│ │ │ │
▼ ▼ ▼ ▼
WARMUP RECALL RECALL RECALL
inject budgeted index score against prompt text
(correction > state > knowledge) gate ≥ 3, top 3, once per session
(no prompt yet → no scoring) (dedup: won't re-inject same memory)
Warmup loads standing defaults before you act. Recall surfaces topic-specific knowledge when you mention it.
~/.claude/memory-store/
├── memories/
│ ├── prefer-aws-cli.md ← one file per memory
│ ├── split-shell-commands.md
│ └── payments-localstack.md
├── INDEX.md ← auto-generated browse mirror
├── state/
│ ├── cursors/{session}.json ← byte offset already distilled
│ ├── injected/{session}.json ← slugs already injected this session
│ └── locks/{session}.lock ← distiller overlap guard
└── logs/
└── memory.log ← structured, greppable audit trail
- You can edit memory files by hand — Engram re-reads from disk on every prompt
INDEX.mdis a browse mirror only; the runtime never reads it- State is garbage-collected after 7 days; the log rotates at 2 MB
- Everything in one folder — trivial to inspect, trivial to delete
| Command | What it does |
|---|---|
/engram status |
Memory counts, store path, recent distiller activity |
/engram search <terms> |
Rank memories using the same scorer recall uses |
/engram forget <slug> |
Delete a memory and rebuild the index |
/engram clear [--all] |
Wipe all memories. Flags: --state, --logs, --all, --dry-run |
/engram clear-logs |
Clear only memory.log; memories untouched |
/engram prune |
Drop empty/untitled orphan memories |
/engram reindex |
Rebuild INDEX.md from memory files |
/engram doctor |
Self-diagnostic: store, hooks, log health |
$ /engram search shell commands
query tokens: shell, commands
→ 14 split-shell-commands (correction·global) — Split shell commands instead of chaining with &&
3 use-make-targets (knowledge·myproject) — Use make targets for common operations
The → marks memories that would be injected (score ≥ 3 and within the top 3).
All lifecycle operations go through one script:
python3 scripts/manage.py install # register hooks in settings.json
python3 scripts/manage.py verify # exit 0 if healthy, 1 if not
python3 scripts/manage.py repair # fix broken install in-place
python3 scripts/manage.py uninstall # remove hooks, data, config
python3 scripts/manage.py migrate # move from plugin system to direct hooksThe shell wrappers (install.sh, update.sh, uninstall.sh) still work —
they delegate to manage.py.
| Env var | Default | Effect |
|---|---|---|
CLAUDE_MEMORY_HOME |
~/.claude/memory-store |
Store location. Set per-project to isolate stores. |
CLAUDE_CODE_DISABLE_AUTO_MEMORY |
(unset) | Set to 1 to disable native auto-memory (set by installer). |
CLAUDE_MEMORY_FAKE_LLM |
(unset) | Path to canned JSON — distiller reads it instead of calling Haiku (for testing). |
Start with /engram doctor — it checks everything in one shot:
Engram doctor
=============
store:
ok /Users/you/.claude/memory-store
ok store is writable
distiller:
ok distiller.py exists
ok 'claude' CLI found on PATH
hooks:
ok hooks/hooks.json exists
ok 5 hook(s) configured across 5 event(s)
log health:
ok 42 line(s) in memory.log
ok no errors in log
locks:
ok no locks held
commands:
ok /Users/you/.claude/commands/engram.md
10/10 ok, 0 warning(s), 0 error(s)
| Symptom | Fix |
|---|---|
| 0 memories after install | Normal — first capture happens after a substantive turn. |
| No memories ever appear | Check claude CLI is on PATH and authenticated. grep ERROR in the log. |
| Duplicate injections | Native auto-memory is still on. Set CLAUDE_CODE_DISABLE_AUTO_MEMORY=1. |
| Hooks not firing | Run python3 scripts/manage.py verify then repair if unhealthy. |
| Moved the repo | Run python3 scripts/manage.py repair — it re-registers the new paths. |
grep WARMUP ~/.claude/memory-store/logs/memory.log # session warm-up
grep RECALL ~/.claude/memory-store/logs/memory.log # per-prompt injection
grep ERROR ~/.claude/memory-store/logs/memory.log # failures
grep CREATE ~/.claude/memory-store/logs/memory.log # new memories capturedEvery event is keyed by session= so concurrent terminals stay legible.
git clone https://github.com/VictorBodnar/engram.git
cd engram
python3 scripts/manage.py install # hooks point at source; edits are live
bash tests/smoke.sh # 36 offline unit tests
bash tests/lifecycle.sh # 36 end-to-end memory lifecycle tests
bash tests/manage.sh # 30 install/repair/uninstall tests102 total tests, all offline (no network, no live LLM).
PR workflow: every PR runs all test suites + config validation via GitHub Actions.
On merge to main, the pipeline auto-tags and creates a GitHub release from the
version in .claude-plugin/plugin.json. Bump the version in your PR.
- Python 3.9+ (stdlib only — zero third-party packages)
claudeCLI on PATH, authenticated (reuses Claude Code's own auth — no separate API key)- Claude Code with hooks support
MIT — Victor Bodnar