Memory Lab
Local-first Agent Knowledge curation and evidence search for Claude Code, Codex, OpenClaw, and Hermes.
Obsidian Markdown under Agent Knowledge/ is the curated source of truth. Memory Lab's SQLite evidence index is disposable and rebuildable; its legacy memory tables remain available for rollback and inspection during migration.
Memory Lab stores no provider API key. Automatic curation invokes the already-authenticated agent host CLI.
Operating model
Keep these layers separate:
- Capture Notes — append-only Markdown preferences, decisions, lessons, and references; the authoritative durable record.
- Wiki Projection — rebuildable entity, concept, and source-index pages with evidence-backed wikilinks.
- SQLite — a derived search index and legacy rollback surface.
Every new memory belongs to exactly one primary entity: USER, AGENT,
APP, or RUN. Its semantic kind and writer remain separate fields. Existing
project memory maps to APP; Obsidian Markdown remains authoritative.
Agents can add durable knowledge through the append-only memory_remember MCP
tool, the explicit promote CLI workflow, or an installed lifecycle hook whose
isolated host-model curator produces a validated candidate and indexed receipt.
How knowledge is organized
Memory Lab uses one dedicated folder inside an existing Obsidian vault:
Agent Knowledge/. Everything outside that folder stays outside the shared
agent index.
Knowledge is organized along two simple dimensions:
- Who or what it belongs to: a user, agent, app/project, or individual run.
- What kind of knowledge it is: a preference, decision, lesson, or reference.
Agent Knowledge/
├── Users/<user>/ # preferences and user-level decisions
├── Agents/<agent>/ # lessons learned by Codex, Claude Code, and others
├── Projects/<app>/ # project decisions, lessons, and references
├── Runs/<run>/ # knowledge specific to one execution
├── Domains/ # durable knowledge shared across projects
├── Playbooks/ # repeatable operational procedures
├── References/ # shared external pointers and source material
└── Wiki/ # generated navigation; safe to rebuild
The individual Markdown Capture Notes are the durable source of truth. Memory Lab compiles them into a linked Wiki for browsing and a local SQLite index for fast retrieval:
Capture Notes → generated Wiki → SQLite search index
authoritative derived derived
Generated Wiki pages and SQLite can always be rebuilt from the Capture Notes. Corrections append a successor note instead of silently rewriting history.
Install
# Stable release from PyPI:
uv tool install memory-lab-mcp
# Today, and for tracking main (needs git credentials while the repo is private):
uv tool install git+https://github.com/haru3613/memory-lab@main
Then check it:
memory-lab --version
memory-lab stats # prints the store, config file and corpora it resolved
pipx install memory-lab-mcp works the same way. The installed commands remain
memory-lab and claude-memory-lab, so host configs do not depend on the PyPI
distribution name.
Update
uv keeps using the source chosen at install time. Use the matching update
path below.
For a PyPI install:
uv tool upgrade memory-lab-mcp
For a Git install that tracks main, the same command fetches and installs the
latest commit:
uv tool upgrade memory-lab-mcp
For an install made from a local checkout, update that checkout and reinstall it:
git -C /path/to/memory-lab pull
uv tool install --force --reinstall /path/to/memory-lab
To switch a Git or local-checkout install to the latest PyPI release:
uv tool install --force 'memory-lab-mcp@latest'
Then verify the installed version:
memory-lab --version
For pipx, use pipx upgrade memory-lab-mcp. Host configs point to a stable
entry-point path, so none of these updates require re-wiring or re-running
setup. Restart any already-running agent or MCP process to load the new code.
The unpublished development builds used memory-lab as their distribution
name. Migrate those in this order so two distributions never own the same
commands or Python package:
uv tool uninstall memory-lab
uv tool install memory-lab-mcp
One caveat: setup records the entry point beside the interpreter that ran it.
Install with uv tool or pipx and that path is stable. Run setup from a
project venv instead and the hook points into that venv, so recreating it
means re-running setup-hooks.
Upgrading from the still older claude-memory-lab distribution? Uninstall it
before installing memory-lab-mcp for the same reason.
Configure
Corpora are opt-in. An unconfigured install indexes nothing, which is the
privacy default rather than a bug. Create ~/.config/memory-lab/config.toml:
[projects]
my-project = "~/.claude/projects/-Users-me-code-my-project"
# Optional. Found automatically on macOS with iCloud.
[obsidian]
vault = "~/path/to/your/vault"
Only the Agent Knowledge/ folder of a vault is ever read. That boundary is
not configurable.
Wire up your agents
Claude Code and Codex, in one command:
memory-lab init --dry-run # prints the plan, changes nothing
memory-lab init
memory-lab doctor # exits non-zero on a broken integration
Remove it
memory-lab uninstall # reports the plan, changes nothing
memory-lab uninstall --apply # disconnect the hosts, remove the config
memory-lab uninstall --apply --purge-store # also delete the SQLite store
Reporting is the default, so every destructive path is one you type on purpose.
--apply removes the tool: it disconnects the maintenance worker and hosts
it finds traces in, then deletes config.toml (backed up first — it is the only
hand-written file in the whole footprint). Existing *.bak-memory-lab-* files
are kept unless you pass
--clean-backups: they are the rollback for the removal you just ran.
That flag covers the host-config backups only — the copies of config.toml and
of the store stay put, since they are the rollback for this run itself.
The store is data, not tool, so it takes --purge-store. Its own memory #1
calls SQLite "a disposable, rebuildable local index", but that is not true of
every row: memories written before the Obsidian-first flow carry no source
event and exist nowhere else, and neither do the capture inbox or the audit
history. The command counts them for you before you decide. When it does delete
the store it takes the -wal/-shm sidecars with it — a .sqlite removed
alone strands committed data in a file nothing will reopen — and it refuses
while any serve-mcp process still holds it, because unlinking a file live
writers hold just drops the inode and lets them recreate it.
Two things it will not do at all, and says so every run:
- It never touches your vault.
Agent Knowledge/is written by this tool, but only theWiki/pages carry a marker identifying them as generated — every capture note shares its frontmatter shape with a hand-written note. There is no test that separates the two, and the notes are the memory itself, so they are reported and left alone. - It does not uninstall the package. It prints the command for however this copy was installed; a process cannot reliably delete the interpreter it is running under.
It also lists any serve-mcp process still holding the old store, grouped by
the host you need to restart — until they exit they keep writing, and can
recreate a store you just deleted.
init refuses rather than writing a command it could not prove runnable, so a
successful run means the hooks work — including from a shell with no PATH. It
names every file it changed and where it backed each one up. --uninstall
reverses it.
OpenClaw and Hermes load a plugin bundle instead of hooks, so they install through their own plugin channels:
openclaw plugins install memory-lab --marketplace https://github.com/haru3613/memory-lab
hermes plugins install haru3613/memory-lab-hermes --enable && hermes memory setup memory-lab
Hermes also needs the package inside its own interpreter — it loads the
provider in-process:
uv pip install --python ~/.hermes/hermes-agent/venv/bin/python memory-lab-mcp.
If that interpreter has the unpublished memory-lab distribution, remove it
before this install; see the migration command in the wiring guide.
memory-lab doctor checks all four, including whether a plugin would open a
different database from the one your hooks write to. Details and the reasoning
behind each step: docs/wiring.
Develop
python3.11 -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest
Setup merges four owned lifecycle hook groups into ~/.codex/hooks.json or
~/.claude/settings.json without replacing unrelated hooks. Repeating setup is
safe; remove only Memory Lab's entries with the matching
setup-hooks --host <host> --uninstall.
Codex requires a one-time human trust decision before user hooks execute. After
setup, open Codex, run /hooks, review the five commands marked
MEMORY_LAB_HOOK=1, and approve them. User hooks run outside the sandbox;
Memory Lab does not write Codex's private trust hashes or install a permanent
trust bypass.
The automatic loop is:
SessionStart: return setup context immediately, then kick a per-store macOS LaunchAgent. Launchd owns the bounded curation and Wiki repair process, so it survives a short agent session; a five-minute interval is the fallback.UserPromptSubmit: resolve the active USER, AGENT, APP, and RUN identities, search their exact union, and inject relevant context.Stop: persist a bounded, secret-redacted user/assistant transcript capture and return without authenticating or starting a curator.PreCompact: reset the automatic-recall lease; Claude Code also captures before context compaction.
Tool output is excluded from captures. Obsidian is updated only when
memory_remember returns an indexed receipt; failures remain pending in SQLite
and retry in a later maintenance run, up to five attempts before the capture is
marked failed and dropped from the retry queue. UserPromptSubmit never runs
the curator — it is the latency-sensitive path, so draining is left to
the launchd-owned worker. Stop likewise only writes the queue. Concurrent
maintenance exits instead of waiting behind a slow iCloud-backed vault scan.
An unrecognized Git repository with an
origin remote is onboarded
automatically; unsafe or unidentifiable directories receive explicit manual
guidance instead of silently skipping the memory loop. Claude's curator runs in
safe mode with tools and session persistence disabled.
Obsidian vault contract
Memory Lab traverses exactly one top-level folder in the existing vault:
Agent Knowledge/
├── _Index.md
├── Users/
├── Agents/
├── Runs/
├── Projects/
├── Domains/
├── Playbooks/
├── References/
├── Templates/
└── Wiki/ # generated; safe to rebuild
├── Entities/
├── Concepts/
└── Synthesis/ # compatibility path for generated source indexes
Everything else is outside the shared agent index, including Journal/, Private/, Inbox/, Archive/, Side Projects/, Strategies/, and Backtests/.
A Capture Note should cover one durable claim and uses the versioned v1
contract below. The opaque id stays stable if the file moves. Optional
source_refs are a flat list of durable provenance strings.
---
schema_version: 1
id: mem-12345678-1234-4abc-8def-1234567890ab
type: decision
project: memory-lab
entity_type: app
entity_id: memory-lab
writer_agent: codex
status: active
# Optional on a successor note:
# supersedes: mem-00000000-0000-4000-8000-000000000000
created: 2026-07-26T00:00:00+00:00
source_refs:
- "issue:#53"
---
The shared parser, validator, and renderer govern MCP writes, ingest, and Wiki compilation. A malformed v1 note fails before derived-state mutation. Existing legacy active Capture Notes remain readable and are not rewritten or assigned synthetic IDs.
The canonical lifecycle statuses are active, superseded, and retracted.
To replace a claim, append a new Capture Note with supersedes set to the
predecessor's stable ID; the predecessor file stays unchanged. The graph makes
the predecessor effectively superseded. To withdraw a claim without a
replacement, append a retracted note that supersedes the current head.
Lifecycle links must stay in the same entity scope and memory type. Dangling
links, self-links, cycles, and forks fail before SQLite or Wiki mutation.
Recall and Wiki source indexes expose only effective active heads. Superseded and
retracted notes remain in raw evidence, audit history, and the Wiki History
section. Rebuild derives the same lifecycle state from Markdown.
The vault defaults to:
~/Library/Mobile Documents/iCloud~md~obsidian/Documents/
Override it with MEMORY_LAB_OBSIDIAN_DIR for fixtures or another vault.
Obsidian refreshes Markdown changed by external tools, and iCloud handles device-to-device vault sync. Memory Lab therefore does not run a file watcher or bidirectional sync daemon.
Compile the Wiki Projection
Preview the deterministic projection without writing:
memory-lab wiki compile
Apply it and refresh the SQLite evidence index:
memory-lab wiki compile --apply
memory-lab wiki lint
The compiler never edits Capture Notes. It creates scope pages with Current and
History sections, deterministic source indexes of current claims, and
concept pages from explicit repo, workflow, tool, and ticket mentions. The
projection records concept occurrences, explicit scope membership, lifecycle
history, and source citations. It does not infer a typed relationship from
concepts in the same note or from retrieval similarity. Re-running compilation
is idempotent, removes only stale pages marked generated_by: memory-lab-wiki,
and refuses symlinked or unowned output paths.
Onboard a project
Run onboarding once from a Git repository:
memory-lab onboard --repo /path/to/your/repo \
--title CardDex --alias cardex-template
The command derives the canonical project ID from the origin remote (for example, haru3613-carddex), creates Agent Knowledge/Projects/carddex/Overview.md without overwriting an existing note, updates ~/.mem0/project_map.json, and incrementally indexes the vault. Use MEMORY_LAB_MEM0_PROJECT_MAP to override the map path.
Promote curated knowledge
Promotion is explicit and dry-run by default:
memory-lab promote --project haru3613-carddex --kind decision \
--title "Use source-native prices" --from-file /tmp/decision.md
Review the rendered note, then repeat with --apply. The command writes a
dated APP note under the onboarded project's Preferences/, Decisions/,
Lessons/, or References/ folder, refuses to overwrite an existing file, and
refreshes the derived SQLite index.
Ingest Obsidian evidence
Incrementally ingest new or changed Markdown notes:
memory-lab ingest-obsidian
Normal ingestion skips unchanged files and deliberately does not prune missing notes. A partial iCloud sync therefore cannot wipe previously indexed evidence.
When changing the trust boundary or intentionally rebuilding the disposable Obsidian corpus, use the explicit rebuild mode:
memory-lab ingest-obsidian --rebuild
Rebuild preflights every approved note before opening the replacement transaction, then replaces only project_key='obsidian' evidence. The memory layer is preserved: links to retained note paths are remapped to their new evidence rows, while provenance for removed paths is retained in memory metadata. If an existing corpus is populated but fewer approved notes are available than MEMORY_LAB_OBSIDIAN_REBUILD_MIN_NOTES (default 1), the rebuild aborts before deleting anything.
Only .md files are imported. Allowlisted-root symlinks and nested paths that resolve outside Agent Knowledge/ fail closed. Empty notes also fail before writes. Searchable text passes through secret redaction before FTS indexing, but credentials and private material still belong outside Agent Knowledge/.
Ingest Claude conversation evidence
memory-lab ingest --all --incremental --index-entities --extract-candidates
memory-lab stats
Claude ingestion is restricted to the corpora listed under [projects] in
~/.config/memory-lab/config.toml. An unconfigured install ingests nothing.
Legacy auto-memory import
This migration-only command imports selected one-fact Markdown files into the legacy memory tables. It is retained for rollback compatibility, not for new cross-agent knowledge; use onboard and promote for that.
Review a read-only plan first:
memory-lab import-auto-memory \
--dir ~/.claude/projects/<project>/memory \
--project cardex-template \
--include feedback_example.md \
--include reference_example.md \
--dry-run
Rerun the same command without --dry-run to write. MEMORY.md is always skipped. The importer accepts scalar name, description, type, and originSessionId fields, with type and originSessionId also allowed one level under metadata; folded or otherwise complex YAML values fail preflight. It maps supported Claude memory types into Memory Lab types, stores absolute source-path and content-hash provenance, skips exact duplicates, and supersedes a prior import when the same source changes. It never modifies source files or scans an entire directory automatically. When MEMORY_LAB_REQUIRE_REVIEW=1, a batch where one pending source update depends on another pending source's old content fails preflight; split that dependency into separately reviewed imports.
Search
memory-lab search "Mission Control no auto-merge" \
--project mission-control --top-k 5 --debug
memory-lab search "設計決策" \
--project obsidian --top-k 5
ASCII evidence uses SQLite FTS5/BM25. CJK terms use a scoped escaped substring fallback, with BM25 matches ranked ahead of fallback-only results. Every evidence result includes its source path.
score (memory and evidence results alike) is raw BM25, sign-flipped so higher is better. It is unbounded above and deliberately not normalized to [0,1] — its magnitude moves with store size and query-term rarity, so the same query can score 1e-6 in a five-document store and 9.0 once the store holds hundreds. Treat it as ordering only, within a single response; never as an absolute relevance threshold or a value comparable across calls.
An unscoped or Obsidian-scoped CLI search refreshes changed Markdown before querying. If refresh fails, it warns and searches the last valid index. MCP search reads the existing index and reports freshness without refreshing Obsidian on the request path.
MCP surface
Agents use this server for retrieval and append-only Agent Knowledge curation:
memory-lab serve-mcp --agent claude-code
app_id is the canonical APP identifier and the normal way to scope a call. The
older project argument is deprecated compatibility: it still works, but it
returns a compatibility_warning and cannot name the other scopes.
memory_search(query=..., app_id="haru3613-carddex")searches memory plus CardDex's scoped Obsidian evidence.memory_search(query=..., user_id="harvey", agent_id="codex", app_id="haru3613-carddex", run_id=...)searches the exact union of active entities.memory_remember(app_id="haru3613-carddex", kind="decision", title=..., content=..., source_refs=[...])creates a new Markdown note without modifying existing notes.memory_remember(entity_type="user", entity_id="harvey", kind="preference", title=..., content=..., source_refs=["baseline:core"])writes a user-scoped preference and opts it into the bounded, once-per-session core baseline. Without that exact source ref, the preference remains available only to task-specific recall. Useentity_type/entity_idfor any non-APP scope.memory_list(app_id="haru3613-carddex", status="active")lists what that APP holds.- Deprecated, still supported:
memory_search(query=..., project="carddex")andmemory_remember(project="carddex", ...). Passing bothapp_idandprojectusesapp_id; passingapp_idalongside a disagreeingentity_type/entity_idis refused rather than resolved. memory_search(query=..., project="obsidian")searches all approved Obsidian evidence —obsidianis a corpus selector, not an APP, so it stays onproject.
Evidence is enabled by default. For onboarded projects, the server resolves the folder, canonical ID, and aliases from Projects/<folder>/Overview.md; pass include_evidence=false only when a caller explicitly wants legacy memories alone.
Search responses include a query_id and index_status="not_requested".
freshness and quarantined_sources describe the most recent explicit
refresh; MCP search does not refresh the vault. Memory results retain their
integer memory_id and expose
nullable capture_id. Remember responses include the stable capture_id,
primary entity, note path, SHA-256, indexed evidence event ID, derived integer
memory ID, and indexed status. Identical remember retries are safe and return
status="existing"; same-title requests with different content still fail
instead of overwriting the note.
After upgrading an older index, run one successful search or ingest-obsidian while the vault is available to backfill canonical IDs and aliases. Until then, an offline legacy index can route project evidence only by folder name.
Use memory_remember for explicit requests to remember something and for durable decisions, lessons, and references established during a task. Do not store transient status, speculation, or secrets. Existing notes remain immutable through MCP: update and delete tools are not exposed.
Agent hosts must enforce the start, running, and closeout behavior in the session lifecycle contract; starting the MCP process alone does not prove that an agent read or wrote memory.
Do not share one SQLite store across personal and company trust boundaries. Use a separate --db path for each boundary; see docs/wiring/store-isolation.md. Per-agent setup lives under docs/wiring/.
Evaluate retrieval
# evidence layer (raw notes)
memory-lab eval run --question-set baseline
# memory layer: each question asked in exact English, English paraphrase and
# Chinese, plus hard negatives that must return nothing
memory-lab eval run --question-set memory-v2
The memory set reports recall@1/@k and MRR per phrasing, a hard-negative
false-positive rate, and the same two numbers for the injected set — what the
UserPromptSubmit hook would actually have shown an agent. Run it against a
copy of the store if you care about the query log: the eval itself never writes
to query_log, but nothing else about --db is read-only.
The injection gate keeps hits within 35% of the top score and above an
absolute floor of K * log10(active memories), disabled below 50 memories.
K defaults to 3.0, calibrated against eval/memory-v2.json; override it with
MEMORY_LAB_INJECT_SCORE_FLOOR_K and re-run the memory eval to see the effect on
injected recall and the injected false-positive rate.
Semantic candidates carry a separate floor, because a nearest neighbour always
exists and an unfiltered vector half cannot abstain. MEMORY_LAB_MIN_COSINE
defaults to 0.511 — the p95 of the null distribution for
qwen3-embedding:0.6b on this corpus, so it is not tuned to the eval's hard
negatives. Set it to 0 for the no-floor arm of a sweep. Changing
MEMORY_LAB_EMBED_MODEL invalidates the default; re-derive it. Derivation and
sweep: eval/baselines/2026-08-25-memor-73-abstention.md.
That cosine floor ranks memory_search only. Automatic recall still injects
on lexical BM25 plus literal_cjk. Hybrid CJK recall@1 is not injected recall.
See docs/adr/0002-semantic-layer-owns-search-not-inject.md.
Generated SQLite databases and eval reports live under gitignored data/.
Read-only UI
The Nuxt 3 viewer under ui/ browses memories, evidence, and stats. Its SQLite driver is read-only.
cd ui
npm install
npm run dev
It defaults to ../data/claude-memory-lab.sqlite, which is the legacy store
location. The CLI may resolve a different one on a fresh install, so point the
viewer explicitly at whatever memory-lab stats reports:
MEMORY_LAB_DB=/path/to/store.sqlite npm run dev
Legacy review actions remain CLI-only for rollback/admin use.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file memory_lab_mcp-0.4.4.tar.gz.
File metadata
- Download URL: memory_lab_mcp-0.4.4.tar.gz
- Upload date:
- Size: 165.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
be38db1c3b23a4d0e56390c63edb91727693cd559bc782896a6839d1f18c8a08
|
|
| MD5 |
35aa62b5c736fadbc40f11be530d1b07
|
|
| BLAKE2b-256 |
f798ba8bf9209e5fefd03d8def93ef6789fafa9f5662ada34a03b6967edda514
|
Provenance
The following attestation bundles were made for memory_lab_mcp-0.4.4.tar.gz:
Publisher:
release.yml on haru3613/memory-lab
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
memory_lab_mcp-0.4.4.tar.gz -
Subject digest:
be38db1c3b23a4d0e56390c63edb91727693cd559bc782896a6839d1f18c8a08 - Sigstore transparency entry: 2662358167
- Sigstore integration time:
-
Permalink:
haru3613/memory-lab@6ade3bb16de5f845c219bd828a10456c5cb48afa -
Branch / Tag:
refs/tags/v0.4.4 - Owner: https://github.com/haru3613
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@6ade3bb16de5f845c219bd828a10456c5cb48afa -
Trigger Event:
push
-
Statement type:
File details
Details for the file memory_lab_mcp-0.4.4-py3-none-any.whl.
File metadata
- Download URL: memory_lab_mcp-0.4.4-py3-none-any.whl
- Upload date:
- Size: 168.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7b181b6d940b314df7fd8ea5580e11503f98f123037354ccaa7df6f6c767f7c7
|
|
| MD5 |
5e71b2a1871060621ccb7466f5ef953e
|
|
| BLAKE2b-256 |
04f7897399f45a86e2c079df7dbd7c1386ab2f2c006f1d814687ee1222d4206b
|
Provenance
The following attestation bundles were made for memory_lab_mcp-0.4.4-py3-none-any.whl:
Publisher:
release.yml on haru3613/memory-lab
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
memory_lab_mcp-0.4.4-py3-none-any.whl -
Subject digest:
7b181b6d940b314df7fd8ea5580e11503f98f123037354ccaa7df6f6c767f7c7 - Sigstore transparency entry: 2662358181
- Sigstore integration time:
-
Permalink:
haru3613/memory-lab@6ade3bb16de5f845c219bd828a10456c5cb48afa -
Branch / Tag:
refs/tags/v0.4.4 - Owner: https://github.com/haru3613
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@6ade3bb16de5f845c219bd828a10456c5cb48afa -
Trigger Event:
push
-
Statement type: