Skip to the content.

Run your memory store through fak

mem0, Letta, and Zep/Graphiti own the part agents actually buy: fact-extraction, embeddings, semantic recall, per-user scoping. They do not own a write barrier. Every fact an LLM extracts is promoted to the durable store by default, a delete_all fires on whoever asks, and a recalled memory flows back into context with no trust check. mem0’s own docs concede there is no request middleware and no pre-write hook.

fak is the layer beneath: a reference monitor at the agent’s syscall boundary. It keeps the store’s retrieval engine intact and adds a default-deny floor in front of every memory operation. This guide wires mem0’s local OpenMemory MCP server behind fak guard so the gate is invisible to your agent and you change no mem0 code.

fak does not replace your memory store. It has no embedder, no vector search, no fact-extraction pass. The play is composition: mem0 keeps recall; fak adds the gate, the audit trail, and the trust screen on the way back in. The durability-classification thesis behind this lives in CONTEXT-IS-NOT-MEMORY.md; this page is the runnable gate.


What the gate gives you

Three things mem0 has no mechanism for, with the policy in examples/mem0-openmemory-policy.json:

  1. Fail-closed writes. add_memories is allowed, but an oversized payload (a context dump masquerading as a “fact”) is refused OVERSIZE, and a secret-shaped payload (a private-key header, an AKIA… / sk-… / xoxb-… token) is refused SECRET_EXFIL before it can be laundered into durable memory.
  2. Capability-gated destruction. delete_all_memories, delete_memory, and reset_memory are denied by structure. A prompt-injected agent that is talked into wiping the store still can’t — the refusal is on the tool name, independent of what the model was convinced to do.
  3. Trust-gated read-back. This one is automatic and needs no policy. Under fak guard, a search_memory result is a tool result, so it routes through the kernel’s result-side floor (/v1/fak/admit) and is quarantined if it carries an injection or a secret — before the recalled bytes reach the model. mem0 returns stored text with no screen; this closes indirect prompt-injection through the memory store (OWASP Memory Poisoning T1), and it does not depend on any classifier judgment.

Memory-store integration checklist

Use this checklist when the store is not mem0/OpenMemory, or when you are adding the deeper durability barrier described in CONTEXT-IS-NOT-MEMORY.md. The shape is the same for mem0, Letta, Zep/Graphiti, LangMem, and custom MCP memory tools: the memory store keeps its retrieval engine; fak owns the boundary where an agent writes, recalls, deletes, or promotes memory.

  1. Inventory every memory surface. Name the exact tool or API operation for writes, recalls, lists, deletes/resets, and background consolidation. If the agent can still call the store through an SDK path that bypasses fak guard, that path is not gated.
  2. Put the effectful surface on the proxied stream. Prefer an MCP tool surface or an equivalent wrapper where the model proposes add, search, delete, and promote as visible tool calls. fak cannot gate a direct library call it never sees.
  3. Declare an exact capability floor. Allow the smallest write/read/list set the agent needs. Deny broad destructive tools such as delete_all, reset, and bulk namespace wipes by default. Add argument rules for oversized writes, secret-shaped payloads, and missing tenant/user scope.
  4. Classify writes before persistence. Every write candidate should carry source principal, source digest, scope, and a durability class: turn, session, bounded, or durable. Unclassified observations default to turn at the live write boundary, so they can be used in context without becoming long-term memory.
  5. Make promotion reviewable. A turn or session observation should create a proposal or audit event, not a durable fact. Promote only when the candidate is explicit, user-confirmed, corroborated, or an established pattern. A one-off remark like “I’m tired today” is not enough evidence to mint “user prefers terse answers.”
  6. Re-admit recalls before they re-enter context. A recalled item is untrusted tool output. Route it through result admission, preserve the source digest and durability metadata, enforce any validity window, and inject the smallest useful fact rather than a raw transcript chunk.
  7. Gate deletes and invalidations separately. Single-item deletion, temporal invalidation, and broad reset are different capabilities. Prefer invalidating or closing a validity interval where the store supports it; require an explicit higher capability for destructive broad deletes.
  8. Journal the decision, not the prose. Record the tool name, verdict, reason, durability, source digest, scope, and store-side id. A later audit should be able to answer why the fact was written, recalled, refused, promoted, invalidated, or deleted without trusting a worker’s natural-language status line.

Provider-specific routing:

Store Write path to gate Recall path to gate Delete / invalidation path Promotion rule
mem0 / OpenMemory MCP add_memories search_memory, list_memories Deny delete_all_memories; narrowly gate single-item deletes if exposed Do not let every extracted fact become durable; require a durability tag before add_memories
Letta Memory-block edits and archival-memory inserts Archival-memory search plus any always-visible memory block read-back Gate block overwrites and archival deletes as separate capabilities Treat block rewrites as durable promotions; require explicit/corroborated evidence before updating a standing profile
Zep / Graphiti Episode ingestion and fact/edge upserts Temporal fact reads, graph searches, and episode read-back Prefer validity-window closure over hard delete Preserve valid_at / invalid_at style metadata; a bounded fact must be queried as-of, not read as timeless
LangMem / LangGraph memory manage_memory-style writes and background consolidation writes search_memory-style lookups Gate namespace deletes and background cleanup separately Run background consolidators through the same write gate; they are not trusted just because they are offline
Custom MCP memory tools Any MCP tool that writes or mutates store state MCP tool results and memory resources Separate delete, reset, and namespace admin tools in policy Require the tool schema to expose scope, source digest, and desired durability, or fail closed to a proposal

The important invariant is boring: no memory side effect happens because the model said it was useful later. The write path needs a capability verdict, the recall path needs a result-admission verdict, the delete path needs its own destructive capability, and the promotion path needs evidence that the fact is actually durable.


Prove the floor before you wire anything (no model, no mem0, no key)

The floor is the same code whether a model is in the loop or not, so you can verify every verdict offline with fak preflight. With Go 1.26+ and a clone:

go build -o fak ./cmd/fak
POL=examples/mem0-openmemory-policy.json

fak policy --check "$POL"                                                              # manifest valid

fak preflight --policy "$POL" --tool search_memory       --args '{"query":"prefs"}'   # ALLOW
fak preflight --policy "$POL" --tool add_memories        --args '{"text":"the user prefers afternoon meetings"}'  # ALLOW
fak preflight --policy "$POL" --tool delete_all_memories --args '{}'                   # DENY  POLICY_BLOCK
fak preflight --policy "$POL" --tool delete_memory       --args '{"memory_id":"x"}'   # DENY  POLICY_BLOCK
fak preflight --policy "$POL" --tool add_memories        --args '{"text":"my key AKIAIOSFODNN7EXAMPLE"}'          # DENY  SECRET_EXFIL
fak preflight --policy "$POL" --tool not_a_listed_tool   --args '{}'                   # DENY  DEFAULT_DENY

Captured verbatim from the binary (the add_memories oversize case uses a 9 KB text):

verdict=ALLOW reason=NONE          by=monitor    # search_memory
verdict=ALLOW reason=NONE          by=monitor    # add_memories (normal fact)
verdict=DENY  reason=POLICY_BLOCK  by=monitor    # delete_all_memories
verdict=DENY  reason=POLICY_BLOCK  by=monitor    # delete_memory
verdict=DENY  reason=OVERSIZE      by=monitor    # add_memories (9 KB text)
verdict=DENY  reason=SECRET_EXFIL  by=monitor    # add_memories (AKIA… token)
verdict=DENY  reason=DEFAULT_DENY  by=monitor    # an unlisted tool

A refusal cites a named, closed-vocabulary reason, not a model judgment — so the gate is a property you can diff and test, not a prompt you hope holds.


Wire it live, under fak guard

  1. Run mem0’s OpenMemory MCP server locally (FastAPI on :8765; Docker compose, local Qdrant — see mem0’s OpenMemory docs). It exposes the tools add_memories, search_memory, list_memories, delete_all_memories.

  2. Add it to your agent as an MCP server (the agent’s own tool), exactly as you would without fak.

  3. Launch the agent through the gate, with this policy as the floor:

    fak guard --policy examples/mem0-openmemory-policy.json -- claude
    

Now every memory tool call the model proposes surfaces on the proxied stream, crosses k.Decide, and is dropped / repaired / allowed before your agent dispatches it to OpenMemory. On exit, fak guard prints the tally:

fak guard: 47 kernel decision(s) — 44 allowed, 3 denied, 0 repaired, 1 quarantined
  blocked: POLICY_BLOCK   x2     # a delete_all the model proposed
  blocked: SECRET_EXFIL   x1     # a token it tried to memorize

For a durable, hash-chained record of every memory decision, add an audit journal:

FAK_AUDIT_JOURNAL=~/mem-audit.jsonl fak guard --policy examples/mem0-openmemory-policy.json -- claude

Honest limits — read before you rely on it


Where this sits among the integration paths

This page is the cheapest of three ways fak composes with a memory store:

Path Effort What it adds This page
Proxy / MCP interception S The gate above, zero store-side code
Durability write-barrier in front of add() M Expire-by-default promotion gate (warn-first; leans on the classifier) CONTEXT-IS-NOT-MEMORY.md
Store as a memq backend L fak’s deterministic, caps-gated, no-hard-delete algebra over the store’s recall internal/memq

The honest framing across all three: fak is security and governance over an unchanged memory store, not better recall. For a benign-drift threat model it is overhead; for a memory-poisoning, compromised-agent, or delete_all threat model it is the refusal an attacker can’t talk the agent out of.

Discover fak-native memory tools

If the agent needs to discover fak’s own memory surface instead of reading this page, query the self-feature catalog:

fak feature query memory --detail fak_memory_run --json

Over MCP, call fak_feature_query with {"query":"memory"}. It returns lightweight cards for fak_memory_drivers, fak_memory_explain, fak_memory_run, and the registered memory drivers. Discovery is read-only; fak_memory_run still defaults to apply=false, so effectful memory changes remain proposals unless the caller explicitly supplies the apply capability. The planning spine is SELF-FEATURE-QUERY-SPINE-2026-06-30.md.


Cross-references