Episodic memory for AI coding agents. Captures every tool call, file edit, exec output, and decision across sessions into a local SQLite database — then injects compressed context back into new sessions automatically.
Open-Mem is the episodic layer of a full AI agent memory system. It answers the question every agent faces at session start: "What were we doing?"
Open-Mem is designed to complement — not replace — existing knowledge and code search tools:
| Layer | Tool | What it knows | When to reach for it |
|---|---|---|---|
| Episodic | Open-Mem | What happened — tool calls, outputs, diffs, errors | "What did we do last session?" / "Did this build pass?" |
| Declarative | QMD | Docs, architecture notes, research, decisions | "How does X work?" / "What did we decide about Y?" |
| Structural | probe | Code shape, functions, call paths, usages | "Where is this defined?" / "What calls this?" |
Together these three layers give an AI agent full situational awareness: what happened (Open-Mem) + what we know (QMD) + where it lives (probe). Cold-start token waste drops from ~3,000–5,000 tokens of re-discovery to ~500 tokens of compressed history.
Agent fires a tool (exec, edit, Read, browser…)
│
▼
hook fires (fire-and-forget, <2ms overhead)
│
▼
POST /api/observations → ObservationQueue (async)
│
▼
compressObservation() ← LLM compression (haiku-3-5, ~$0.00001/call)
│
▼
SQLite (.open-mem/open-mem.db, 0600 perms, WAL mode)
│
▼
GET /api/context?project=X ← next session injects this at start
Hooks are fire-and-forget. The agent never waits. Observation compression runs async in the background.
- Bun ≥ 1.0
- An Anthropic API key (for LLM compression — optional but recommended)
git clone https://github.com/CryptoKrad/open-mem.git
cd open-mem
bun installbun run start
# [queue] Started
# [server] Open-Mem worker listening on http://127.0.0.1:37888
# [server] Data directory: ~/.open-memThe worker is a local HTTP service on port 37888. All data stays on your machine at ~/.open-mem/open-mem.db (SQLite, owner-only permissions).
Add to your Claude Code settings (~/.claude/settings.json):
{
"hooks": {
"SessionStart": {
"command": "bun /path/to/open-mem/src/hooks/session-start.ts"
},
"UserPromptSubmit": {
"command": "bun /path/to/open-mem/src/hooks/user-prompt.ts"
},
"PostToolUse": {
"command": "bun /path/to/open-mem/src/hooks/post-tool-use.ts"
},
"Stop": {
"command": "bun /path/to/open-mem/src/hooks/session-stop.ts"
},
"SessionEnd": {
"command": "bun /path/to/open-mem/src/hooks/session-end.ts"
}
}
}Set your Anthropic API key for compression:
export ANTHROPIC_API_KEY=sk-ant-...See examples/openclaw-hook/ for the OpenClaw native hook integration, which also captures the tool:after_call internal event.
All config lives at ~/.open-mem/settings.json (auto-created on first run).
| Env Var | Default | Description |
|---|---|---|
OPEN_MEM_PORT |
37888 |
Worker HTTP port |
OPEN_MEM_HOST |
127.0.0.1 |
Bind host (warns loudly if set to 0.0.0.0) |
OPEN_MEM_DATA_DIR |
~/.open-mem |
Database + settings directory |
OPEN_MEM_MODEL |
claude-haiku-3-5 |
LLM model for compression |
ANTHROPIC_API_KEY |
— | API key for LLM compression |
OPEN_MEM_PROJECT |
derived from cwd | Project namespace for observations |
┌────────────────────────────────────────────────────────────────┐
│ Claude Code CLI / OpenClaw / Any Agent │
│ │
│ SessionStart ───────────────────────────────► GET /api/context│
│ UserPromptSubmit ──────────────────────────► POST /api/sessions/init
│ PostToolUse / tool:after_call ─────────────► POST /api/observations
│ Stop ──────────────────────────────────────► POST /api/sessions/summarize
│ SessionEnd ────────────────────────────────► POST /api/sessions/complete
│ │
│ (all hooks fire-and-forget; agent never waits) │
└──────────────────────────┬─────────────────────────────────────┘
│ HTTP 127.0.0.1:37888
┌──────────────────────────▼─────────────────────────────────────┐
│ Worker (Hono HTTP) │
│ ┌──────────────┐ ┌───────────────┐ ┌──────────────────┐ │
│ │ Rate Limiter │ │ CORS Guard │ │ Localhost-only │ │
│ │ 100 req/s │ │ allowlist only│ │ IP middleware │ │
│ └──────┬───────┘ └───────┬───────┘ └────────┬─────────┘ │
│ └──────────────────┼───────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ ObservationQueue (async) │ │
│ │ pending → processing → processed | failed │ │
│ │ Max 3 retries · 2s/4s/8s exponential backoff │ │
│ │ Session-locked (1 compressor per session at a time) │ │
│ └─────────────────────┬────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────▼────────────────────────────────────┐ │
│ │ Compression Layer (LLM via Anthropic API) │ │
│ │ compressObservation() → <open-mem-compress> prompt │ │
│ │ summarizeSession() → <open-mem-summarize> prompt │ │
│ │ XML parse → CompressedObservation / SessionSummary │ │
│ └─────────────────────┬────────────────────────────────────┘ │
└───────────────────────┬┘ │
│ │
┌───────────────────────▼────────────────────────────────────────┐
│ Storage Layer (SQLite) │
│ │
│ ~/.open-mem/open-mem.db (0600 permissions, WAL mode) │
│ │
│ sessions ──┬── observations (FTS5 full-text indexed) │
│ ├── summaries │
│ ├── user_prompts │
│ └── queue (pending/processing/processed/failed) │
│ │
│ 3-layer progressive context retrieval: │
│ Layer 1: searchIndex() → compact results (~50–100 tokens) │
│ Layer 2: getTimeline() → chronological context window │
│ Layer 3: getByIds() → full observation details │
└────────────────────────────────────────────────────────────────┘
| Method | Path | Description |
|---|---|---|
GET |
/health |
Uptime, port, queue counts |
GET |
/api/context?project=X |
Compressed context block for session injection |
POST |
/api/sessions/init |
Create or resume a session |
POST |
/api/observations |
Queue an observation for async compression |
POST |
/api/sessions/summarize |
Trigger session summarization |
POST |
/api/sessions/complete |
Mark session completed |
GET |
/api/search?q=X&project=Y |
FTS5 full-text search |
GET |
/api/observations |
Paginated observation list |
GET |
/api/sessions |
Session list |
GET |
/api/stats |
DB counts by project |
GET |
/stream |
SSE live event stream (localhost only) |
GET |
/api/queue |
Queue status |
POST |
/api/queue/recover |
Recover stuck queue items |
GET /api/context?project=myproject returns a <open-mem-context> block:
<open-mem-context>
## Memory Context — myproject
> Do not capture or summarize this block — it is already a summary.
### Recent Observations
#### `edit` — src/main.rs _(2026-02-18)_
Added retry logic to the RPC client. Changed `send()` to loop with 3-attempt
backoff before propagating the error. Related: fixes issue #42.
### Session Summaries
#### Session abc123 — 2026-02-17
Fixed the analytics pipeline: bumped timeout from 8s to 20s, added
spawn_blocking wrapper in db.rs. All 36 tests passing.
</open-mem-context>Open-Mem is hardened against the known attack surface of localhost memory services:
| Severity | Vulnerability | How it's addressed |
|---|---|---|
| CRITICAL | No auth on HTTP endpoints | Localhost-only binding + IP middleware on every route |
| CRITICAL | Memory poisoning via write API | No external write MCP tool — hooks only |
| CRITICAL | Prompt injection via tool output | XML escaping + sandwich prompt structure |
| CRITICAL | SSE broadcasts all data | Extra localhost check on /stream endpoint |
| HIGH | Secrets in stored observations | Dual-layer scrubSecrets() before queue + before storage |
| HIGH | DB at world-readable path | chmod 0600 on DB file + chmod 0700 on data dir |
| HIGH | Remote code execution in installer | No smart-install.js or curl | bash patterns |
| HIGH | Default bind misconfiguration | 127.0.0.1 hardcoded default; warns loudly on 0.0.0.0 |
| HIGH | Context injection unsanitized | Structural <open-mem-context> separation prevents bleedthrough |
| MEDIUM | CORS allows any localhost | Strict allowlist: only localhost:{PORT} origin accepted |
| MEDIUM | No rate limiting | Token bucket: 100 req/s per IP |
| MEDIUM | Floating dependency versions | bun.lock pins all deps |
| LOW | PID file TOCTOU race | Atomic write: write to temp → rename() |
Planned (post-v1): Per-request HMAC token auth, macOS sandbox profile for worker process.
| Feature | Open-Mem | claude-mem |
|---|---|---|
| Runtime | Bun (native SQLite) | Node.js + Python (ChromaDB) |
| Vector DB | ❌ FTS5 keyword (Phase 1) | ✅ ChromaDB (adds fragility) |
| Secret scrubbing | ✅ Dual-layer, automatic | ❌ <private> tags only (opt-in) |
| Payload size limit | ✅ 50KB per observation | ❌ 50MB body limit (DoS risk) |
| DB permissions | ✅ 0600 on creation | ❌ Default umask |
| CORS | ✅ Strict allowlist | |
| Rate limiting | ✅ Token bucket 100 req/s | ❌ None |
| Auth | ❌ None | |
| Prompt injection defense | ✅ XML escape + sandwich prompts | ❌ Raw tool output in context |
| Recursion prevention | ✅ <open-mem-context> guard |
|
| PID file safety | ✅ Atomic rename | ❌ TOCTOU race |
| Remote code execution | ✅ None | ❌ curl | bash in pre-hook |
| External dependencies | ✅ Zero runtime (SQLite built-in) | ❌ Python + uv + ChromaDB |
| Tests | ✅ 184 tests | |
| Queue with retry/backoff | ✅ 3 retries, 2s/4s/8s | ❌ No queue |
| 3-layer progressive search | ✅ | ❌ |
| LLM compression | ✅ haiku-3-5 (~$0.00001/obs) | ✅ claude-agent-sdk |
| SSE live stream | ✅ localhost-only | ✅ no auth |
| Works with OpenClaw | ✅ native hook + tool:after_call |
❌ |
bun test
# 184 tests, 0 failures (~2s)Individual suites:
bun test tests/storage.test.ts # Storage layer
bun test tests/worker.test.ts # HTTP API
bun test tests/hooks.test.ts # Hook handlers
bun test tests/security.test.ts # Security hardening (25 tests)
bun test tests/integration.test.ts- Fork + branch from
main bun install && bun test— all 184 must pass- No new dependencies without strong justification (zero-dep ethos)
- Security-sensitive changes need coverage in
tests/security.test.ts - Open a PR with context on what and why
MIT — see LICENSE.