Real-time global intelligence dashboard. AI-powered news aggregation, geopolitical monitoring, and infrastructure tracking in a unified situational awareness interface
Encrypted offline memory vault for MCP agents
Compartment gives agents one encrypted memory vault on the user’s machine, with local search, expiry, tagging, relations, and a desktop panel for locking and unlocking. It plugs into MCP clients and agent setups so what one agent learns can be recalled later by the others, without sending data off the machine.
Builders who want their agent tools to remember past work, preferences, and project facts across sessions.
You can keep agent memory persistent, private, and shared across tools without relying on a cloud service.
What it does
Offline encrypted storage
Keeps memories in one local vault with encryption at rest, including embedding vectors, and no network calls at runtime.
MCP and agent integrations
Connects to Claude Code, Claude Desktop, Cursor, Codex, Gemini CLI, Hermes Agent, OpenClaw, and other MCP clients.
Desktop control panel
Provides a macOS menu bar app, Windows tray app, and Linux window for unlocking, locking, and viewing recent memories.
Dashboard and search
Shows a local dashboard with memory growth, tags, relations, and live search.
Memory rules and recall
Stores one claim per memory with source, date, expiry, importance, tagging, and hybrid semantic plus keyword retrieval.
Hooks and import tools
Includes Claude Code hook integration, import commands, and sync tools for memory capture and recovery.
How to get it
- 1So each record is embedded as overlapping windows of W = 448 tokens with a stride of S =…
windows(d) = ceil( max(0, tokens(d) - W) / S ) + 1 capped at 64 s_vec(d) = max over windows w of d : cos(q, w)
- 2The two channels are alternatives, not addends: either one alone can establish…
P(relevant) = 1 - (1 - p_vec)(1 - p_lex)
- 3and the score is its logarithm, which ranks identically but keeps spreading results near…
score(d) = - w_vec · log(1 - p_vec(d)) - w_lex · log(1 - p_lex(d)) w_vec = 0.75 w_lex = 0.25
- 4Turning a cosine into a probability. An L2-normalized encoder gives cosines that are…
p_vec(d) = clamp( (cos(q, d) - 0.25) / (0.85 - 0.25), 0, 0.88 )
- 5Turning a keyword hit into a probability, without BM25. BM25 measures how well a…
I(t) = log( N / (1 + df(t)) ) N = records in the vault p_lex(d) = ( Σ I(t) for query terms t present in d ) / ( Σ I(t) for all t )
- 6A small rank-agreement term is added, the one thing reciprocal-rank fusion does well,…
+ w_rrf · k · [ 1/(k + rank_vec) + 1/(k + rank_lex) ] w_rrf = 0.10, k = 20
README
Compartment
Encrypted, fully offline memory for AI agents. One vault on your own computer, read and written by Claude Code, Claude Desktop, Hermes Agent, OpenClaw, Cursor, Codex and any other MCP client. No API key, no account, no network, no telemetry.
One-click install (after pip install compartment && compartment init):
Claude Code, Claude Desktop, Hermes Agent and OpenClaw are wired by one
command instead: compartment integrate claude, hermes or openclaw.
Compartment is persistent memory for AI agents, stored on your own computer. What an agent learns in one session is available in every later session, in every project, to every agent on the machine, and nothing leaves the machine.
Each memory is a single claim, recorded with its source and the date it was
learned. Memories can expire: set expires and the memory is removed after
that date. When a preference changes, the new one replaces the old one.
Recall is a hybrid vector and keyword search over an in-memory index. It
answers in about 12 ms and returns only what is relevant.
The embedding model is included in the package. Everything on disk is encrypted, including the embedding vectors, and only your passphrase opens it. A new vault comes with about 6,700 reference facts about hardware, operating systems, ports, encodings and shell tools. They are ordinary memories, and one switch removes them from search.
How it compares with other memory servers
Where each server keeps memory and what protects it, as documented by each project on 2 September 2026. Sources and the full table are in docs/COMPARISON.md; corrections are welcome as a PR against that file.
| Memory at rest | Encrypted | Account / API key | Network at runtime | |
|---|---|---|---|---|
| Compartment | one encrypted file; index in RAM | yes, vectors too | none | none, CI-enforced |
@modelcontextprotocol/server-memory | plaintext memory.jsonl, substring search | no | none | none |
| mem0 (open source) | vector store + LLM-extracted facts; its MCP server is hosted only | not documented | LLM key | LLM calls; telemetry on by default |
| Graphiti (Zep) / Letta | Neo4j / server + database | not documented | LLM key | LLM calls; telemetry on by default |
| claude-mem | local SQLite + Chroma | not documented | sign-in required | account + provider calls; telemetry on by default |
| basic-memory (AGPL) | Markdown + SQLite | not documented | none | telemetry on by default |
| Hindsight (Vectorize) | one container with embedded PostgreSQL | not documented | LLM key (local models configurable) | LLM calls; vendor states no telemetry |
| Supermemory | cloud service, or a self-hosted prebuilt binary | not documented | account (cloud) or LLM key (self-host) | cloud calls; self-host: vendor states no telemetry |
| Cognee | SQLite + LanceDB + Kuzu locally, or cloud | not documented | LLM key | LLM calls; telemetry on by default |
| MemOS | Neo4j + Qdrant self-hosted, or cloud | not documented | LLM key | LLM calls; telemetry on by default |
The memory logic
Almost everything is stored. Only empty turns are dropped. A bare "OK" is a decision, not noise: when the agent asks "Want me to send this reply to the client now?" and the user answers "OK", Compartment stores the decision together with the question it answered. Small talk is kept but ranked last.
Importance is assigned by fixed tiers. Decisions and consent 0.90, personal facts and preferences 0.80, the user's machine and configuration 0.75, other substantive statements 0.55, small talk 0.20. Importance multiplies a match score rather than adding to it, so it breaks near-ties in favour of what matters and can never surface a memory that did not match the question.
One claim per memory, enforced. The store rejects anything longer than
200 characters (the max_memory_chars setting), and anything containing
lists, headings or paragraphs, with an error that says how to split it.
Instructions alone did not work: on a real vault, the median memory written
by an agent was 1,938 characters of bulleted session log. memory_store_many
stores a batch in one call. compartment atomize splits over-limit memories
in an existing vault; each piece keeps the original's dates, and the original
is marked superseded but stays readable by id.
Every memory records its source and date. source is required: "from
chat", "read from pyproject.toml", "web search". discovered is the date the
fact was learned, separate from the date it was saved. Both are appended to
the text as a short clause, for example [web search, 2026-08-01].
Memories can expire. For a fact that stops being true on a known date,
such as a sale price, a booking or a door code, set expires to that date
(2026-09-03) or to a duration (14d, 2w, 3m, 1y). The memory is
removed after that date. compartment expire runs the sweep by hand;
expire_memories turns it off. Most facts should not expire; a wrong expiry
deletes a memory the user wanted.
Facts accumulate; opinions update. A new fact is added beside the
others: the door code changed, a script lives at a path, a release shipped.
An opinion replaces one. When a preference is stored with kind="opinion",
the vault looks for a similar live opinion first. If it finds one, it returns
the old record instead of inserting, and the caller resends with
supersedes=[old id] to replace it, or supersedes=[] to keep both.
Restating a live opinion refreshes its date instead of storing a copy.
Superseded records are removed from search but kept in the audit chain and
readable by id, with a pointer to their replacement. supersedes also works
on facts, for corrections. Opinion ranking weights recency much more than
fact ranking, so the newest opinion wins. compartment opinions audit finds
overlapping live opinions in older vaults and keeps the newest, or reports
them for manual merging.
Capture does not depend on the model. A host that declares its own
memory in its system prompt can override any tool instruction. So
integrate claude installs a PostToolUse hook that writes each memory
file Claude Code saves into the vault, whether or not the model calls the
tool. The hook leaves your other hooks untouched, backs up settings.json
first, always exits successfully so it can never break your editor, and does
nothing while the vault is locked. compartment hook status | install | uninstall, or integrate claude --no-hooks. compartment import-claude
imports anything the hook missed.
Search returns what is relevant, not a fixed number. Compartment returns
every memory whose score holds up against the best result for the same
question, up to a generous cap. The cut is relative because scores are not
comparable between questions: on a real vault, the nonsense query "how to
bake sourdough bread" scored higher than the real query "what did Max decide
about Airtable". A question the vault knows nothing about returns nothing.
Pass top_k to get exactly that many.
Tags are kept current. What a memory is about never changes; what it is
relevant to does. Suppose that while working on a project called Northwind
you learn that the client wants figures before conclusions. The agent tags
the memory northwind. Two years later the same client, now called Harbour,
hires you again, and the agent searches with the tag harbour. The memory
is still true, but a tag filter cannot find it. So a background pass gives
every memory the tags its nearest neighbours in embedding space carry,
weighted by similarity: as Harbour memories accumulate near that old one, it
picks up the harbour tag. Two more signals run alongside: tags that almost
always occur together imply each other, and an existing tag whose phrase
appears in a memory's text is attached. The pass writes only tags, never
text, dates or embeddings. It only adds tags unless you pass --prune,
tags_origin preserves the original tags, and compartment retag --dry-run
shows what would change.
A graph as well as a list. memory_link records a relation: subject,
predicate, object, optionally tied to a memory and to a validity window.
memory_relations answers by entity, by predicate, or as of a date.
Compartment stores and matches relations deterministically; the host model
decides what to link.
Memories are data, not instructions. Recalled memories are wrapped with
a notice that they are stored data. Content from an untrusted source can be
marked quarantined, which adds a warning to every recall of it. The host
agent must still treat memory as data.
One embedding model per vault. The model's SHA-256 is recorded in the
vault and checked on open, so similarity scores stay comparable. To change
model, run compartment reindex --re-embed.
No LLM inside. Embeddings run locally with a bundled 384-dimension int8 ONNX model, in one shared process of about 70 MB that every agent on the machine uses. The host model decides what to store and forget; Compartment captures, encrypts and recalls. That split keeps the offline guarantee absolute and every decision reproducible. With an offline LLM, the whole agent runs with no network.
See what it learned. compartment recent lists the newest memories,
hiding the reference facts so your own memories are visible.
compartment status reports organic_records beside the total.
memory_recent is the same view over MCP.
The app and the dashboard
The same panel on each system: the menu bar on macOS, the notification area on Windows, and an ordinary window on Linux, listed in the applications menu. Linux gets a window on purpose: a tray icon may never appear on GNOME or Wayland, and the control that unlocks your memories must not fail silently.
The panel shows whether the vault is open, how many memories it holds and how many you stored, the three settings worth changing (capture hook, whether reference facts appear in search, auto-lock), which agents are connected with buttons to connect Claude, Hermes Agent or OpenClaw, and the last five memories. You can unlock, lock and change your passphrase there without a terminal. The app keeps no vault in memory; it reads state from the CLI, so it costs nothing when idle. It is meant to be one of the many apps on your computer, not something you have to learn: every function is a button or a switch, and the defaults were chosen by measurement.
The Dashboard button opens the whole vault in your browser: growth over time, the relation graph with every entity named, tags, per-agent counts and live search. It is served from RAM on 127.0.0.1 only, read-only, with no outbound requests.
The mathematics
Everything below is in one file,
src/compartment/ranking.py, used by the
vault, the dashboard and the benchmark. A benchmark score therefore measures
the product itself.
Storage: long memories are embedded in windows
The encoder reads 512 tokens. Text past that is not seen at all, so a long memory used to be searchable only by its opening. On a real 6,705-memory vault, 40% of records exceeded the window and 57.6% of the text was invisible to semantic search.
So each record is embedded as overlapping windows of W = 448 tokens with a
stride of S = 384, giving 64 tokens of overlap so no fact is cut in half,
and the record is scored by its best window:
windows(d) = ceil( max(0, tokens(d) - W) / S ) + 1 capped at 64
s_vec(d) = max over windows w of d : cos(q, w)
Max, not average: a memory is relevant if any part of it is, and an average would penalise a long memory for its other parts. With one window per record this is identical to the old behaviour, so short memories are unaffected. Most memories are short: 6,705 records produced 6,785 windows. Windows are measured in model tokens, not characters, because a character budget is off by a factor of three between prose and a hex digest.
Recall: two channels, combined as evidence
Two indexes answer different questions. The vector index answers what a memory means; the keyword index answers what it says. Their scores are not on the same scale, and combining them is the whole problem.
Until 4.7, Compartment added them. Adding lets a merely-good semantic match outvote conclusive literal evidence: searching a real vault for a commit SHA that appears in exactly one memory returned that memory below ten paraphrases of it, because the sum buried a first-place keyword hit.
The two channels are alternatives, not addends: either one alone can establish relevance. That is a soft OR over independent evidence,
P(relevant) = 1 - (1 - p_vec)(1 - p_lex)
and the score is its logarithm, which ranks identically but keeps spreading results near the top instead of saturating at 1:
score(d) = - w_vec · log(1 - p_vec(d)) - w_lex · log(1 - p_lex(d))
w_vec = 0.75 w_lex = 0.25
Either channel near certainty carries the memory alone; neither can veto the other.
Turning a cosine into a probability. An L2-normalized encoder gives cosines that are comparable across queries, so fixed bounds map them. Per-query min-max normalization would rescale the best hit of a hopeless query up to 1.0 and throw that information away.
p_vec(d) = clamp( (cos(q, d) - 0.25) / (0.85 - 0.25), 0, 0.88 )
The 0.88 ceiling matters. A cosine is a similarity, never an identity: an
encoder can say this is about the same thing, never this is the record you
named. A literal match on a string unique to one memory can. So the semantic
channel is capped below what the literal channel can reach, and the cap is
forced by the weights: the literal channel tops out at
0.25 · -log(1 - 0.999) = 1.727, so 0.75 · -log(1 - cap) < 1.727, giving
cap < 0.90.
Turning a keyword hit into a probability, without BM25. BM25 measures how well a document matches, which does not settle a contest against a semantic hit. What settles it is how unlikely the match was by chance. Each query term carries its self-information over the vault, and a memory scores the fraction of the query's information it accounts for:
I(t) = log( N / (1 + df(t)) ) N = records in the vault
p_lex(d) = ( Σ I(t) for query terms t present in d ) / ( Σ I(t) for all t )
A term unique to one memory is near-conclusive; a term in a tenth of the vault is almost nothing, whatever its BM25. This is what makes literal and semantic hits comparable.
The keyword index is queried as AND first, because an exact phrase is the strongest signal. FTS5's implicit AND requires a nine-word question to appear word for word, so when AND finds nothing it falls back to OR over the informative terms only: anything in more than 10% of records is dropped. That threshold is measured from the vault, not taken from an English stopword list, so it works the same for code, names or other languages.
A small rank-agreement term is added, the one thing reciprocal-rank fusion does well, sized to break ties:
+ w_rrf · k · [ 1/(k + rank_vec) + 1/(k + rank_lex) ] w_rrf = 0.10, k = 20
Importance and recency multiply the score
final(d) = score(d) · ( 1 + w_imp · (2·importance(d) - 1)
+ w_rec · 2^( -age_days(d) / half_life ) )
facts: w_imp = 0.15 w_rec = 0.10 half_life = 180 days, from `created`
opinions: w_imp = 0.15 w_rec = 0.30 half_life = 30 days, from the last
re-affirmation (`affirmed`)
Multiplicative, so a prior can only reorder memories that already matched. An additive prior would let an important memory surface for an unrelated question. A memory that matched nothing scores zero and stays there.
Centred on the 0.5 default, hence 2·importance - 1. Every unweighted
memory carries 0.5, including the thousands of reference facts, so without
centring they would all get the same boost and importance would do nothing.
Centred, an unweighted memory is neutral and only a deliberate weight moves
it.
A fact's recency bonus halves every 180 days from when it was stored. An opinion's halves every 30 days from when it was last re-affirmed, at three times the weight, so the newest opinion on a subject wins.
Retrieval order
Namespace, tag, date and reference-fact filters run after ranking, so a pool sized to the requested number of results could be emptied by them while matching memories sit just past the cut. The pool starts at 200 per channel and widens up to three times when filtering leaves too few. Below 20,000 records the vector search is exact (SIMD matrix math, recall 1.0); above that, HNSW at about 99% recall.
Security and the lock model
The primitives: XChaCha20-Poly1305 encryption on everything at rest,
including embedding vectors, because vectors can be inverted back to text ·
Argon2id key slots, LUKS-style · a key per record, so forget --shred
destroys the key and the content is unrecoverable rather than marked deleted
· an fsync'd sealed journal, atomic compaction, and tested kill -9 recovery ·
a hash-chained audit log (compartment audit verify) · signed vault
manifests and packs · stdio transport with no open ports; the one local
socket is the shared embedding process's Unix socket, in the same private
directory as the unlock credential, carrying text in and vectors out and
never a key · a runtime guard that aborts on any network socket attempt
(--assert-offline), with CI running the whole suite under it on Linux,
macOS and Windows. The full threat model,
including what Compartment cannot protect against, is in
SECURITY.md.
From the app
Everything you do day to day is a button. Unlock asks for your passphrase; Lock closes the vault and clears every stored credential; Change password rekeys it; Auto-lock chooses 15, 30 or 60 idle minutes, or never. Compartment never generates a password, seed or recovery phrase, and holds no credential you do not.
After an unlock the vault stays open across processes, logouts and logins for as long as you leave it, until a restart or power loss, until the auto-lock timer fires, or until you lock it. A restart or power loss always locks it: the unlock credential is the master key wrapped with a random per-boot secret that lives only in kernel memory and is never written to disk, so a new boot cannot open it. A copy of the credential file on its own is useless.
From the command line
The same controls, plus two that only exist here:
compartment unlockandcompartment lockdo what the buttons do. Agents can lock with thememory_locktool. (Vaults from older versions that were issued a recovery phrase still accept it.)compartment 2fa enableadds a second factor: your passphrase plus a keyfile, for example on a USB stick. Both feed Argon2id together, so the requirement is enforced by the cryptography, not by a setting; a stolen vault file plus your passphrase opens nothing without the keyfile. The keyfile's location is remembered, so unlocking feels the same while it is present.compartment unlock --keychainon macOS is an explicit opt-in that survives reboots.
The memory_unlock MCP tool exists but is off by default, because enabling
it puts the passphrase in the model's context.
One vault, many agents, any machine
Without the command line
Every agent on the machine uses the same vault, and none of that needs setting up: the app's Connect an agent buttons wire Claude, Hermes Agent and OpenClaw, and what one agent stores the others recall. Claude, Hermes Agent, Cursor and the CLI can use the vault at the same time: writes are serialised by a file lock, every process notices writes by others and reloads, and each agent has its own identity and namespace. Their servers share one embedding process too, so ten agents cost one model in RAM, and it leaves a few minutes after the last of them does.
A locked vault is one file, memory.vault in the .compartment folder of
your home directory. To move to another machine, lock the vault, copy the
file there, install Compartment and unlock it in the app with your
passphrase.
From the command line
The same move, signed so the recipient can check it, plus the escape hatches:
compartment lock --sign
scp ~/.compartment/memory.vault other-machine:
compartment --vault memory.vault unlock # your passphrase (+ keyfile if 2FA)
lock --sign adds an Ed25519 manifest that the r
Files in the repo
- .claude-plugin
- .github
- add
- docs
- install
- integrations
- packaging
- skills
- src
- tests
- tools
- _config.yml
- .gitattributes
- .gitignore
- CODE_OF_CONDUCT.md
- CONTRIBUTING.md
- Dockerfile
- FORMAT.md
- gemini-extension.json
- GEMINI.md
- glama.json
- LICENSE
- llms-install.md
- mcp.json
- PACKS.md
- plugin.json
- privacy.md
- pyproject.toml
- README.md
- registry.html
- RELEASING.md
- SECURITY.md
- server.json
Discussion (0)
Ask about usage, or say what you built with itSign in to join the discussion.
No comments yet. Be the first to say what this is good for.
More connectors
High-performance code intelligence MCP server. Indexes codebases into a persistent knowledge graph — average repo in milliseconds. 158 languages, sub-ms queries, 99% fewer tokens. Single static binary, zero dependencies.

Universal provider proxy for OpenAI Codex & Claude Code — use any LLM (Claude, Gemini, Grok, DeepSeek, Ollama…) with Codex CLI, App, SDK, and Claude Code
Git-native persistent memory for AI coding agents. Implements Google OKF v0.2 with sub-300µs in-memory BM25 search, embedded MCP server, and progressive disclosure. Slashes token bloat by 80% with zero external databases or dependencies. Built in pure Go.
Local-first code intelligence graph for MCP and CLI. Builds a persistent map of your codebase so AI coding tools read only what matters, with benchmarked context reductions on reviews and large-repo workflows.
Stop your AI from making things up — it proposes, deterministic tools decide, every claim checked against ground truth with evidence. Grounded facts and context survive resets. Reverse engineering is the proving ground. MCP server + CLI.