cd /news/ai-agents/onememory-self-hosted-shared-memory-… · home › topics › ai-agents › article
[ARTICLE · art-157053] src=github.com ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

OneMemory – self-hosted shared memory for AI agents over MCP

OneMemory, a self-hosted shared memory store for AI agents, launched as an npm package (onememory-mcp) that lets Claude Code, Claude Desktop, claude.ai, Claude mobile, Cursor, Copilot, Cline, Continue and n8n share one note store over MCP or plain REST. The package requires Node.js 24 or newer because it relies on Node's built-in node:sqlite, which does not exist in Node 22 or older; users who cannot upgrade are directed to Docker. Running `onememory start` serves the dashboard and REST API on port 8790 and MCP-over-HTTP on port 8791 bound to 127.0.0.1, with all data, tokens and spaces config stored in ~/.onememory (owner-only 0700 on macOS/Linux) so npm updates never touch it.

read13 min views1 publishedOct 25, 2026
OneMemory – self-hosted shared memory for AI agents over MCP
Image: Michielbdejong (auto-discovered)

One small, self-hosted memory store that every AI agent you use can share — over MCP or plain REST, from your desktop, the web, or your phone.

Write a note from Claude Code on your desktop; read it back from Claude on your phone in a fresh chat. Same store for Claude Code, Claude Desktop, claude.ai, Claude mobile, Cursor, Copilot, Cline, Continue, n8n — anything that speaks MCP or HTTP.

Requires Node.js 24 or newer. OneMemory uses Node's built-in node:sqlite, which does not exist in Node 22 or older. Check with node -v. Can't upgrade? Use Docker instead.

npm i -g onememory-mcp                           # 1. install
claude mcp add onememory -- onememory mcp        # 2. wire it into Claude Code (stdio)
onememory doctor                                 # 3. verify Node, database, token

Then, in Claude, say "remember that my staging server is on port 8443", and in a fresh chat ask "what port is my staging server on?". Step 4, only if you want the dashboard, REST API and MCP-over-HTTP (the curl examples below need it): onememory start.

No global install: claude mcp add onememory -- npx -y onememory-mcp mcp. onememory init prints the config for Claude Desktop / Cursor / Copilot; INSTALL.md has every client.

onememory start serves the dashboard and REST API on :8790 and MCP-over-HTTP on :8791 (bound to 127.0.0.1). To reach it from your phone or claude.ai, put an authenticated tunnel (Cloudflare Tunnel + Access, Tailscale, or your own TLS proxy) in front of :8791 — see INFRA.md.

Plain REST works too once onememory start is running (the token is in ~/.onememory/.token):

TOKEN=$(cat ~/.onememory/.token)
curl -X POST localhost:8790/notes -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"title":"Deploy day","body":"Shipped v1.1"}'
curl -H "Authorization: Bearer $TOKEN" "localhost:8790/search?q=deploy"

Everything (database, token, scoped tokens, spaces config) lives in ~/.onememory (Windows: %USERPROFILE%\.onememory), outside the installed package, so npm update and npx cache cleanups never touch it. Set ONEMEMORY_HOME to put it elsewhere, or ONEMEMORY_DB_PATH for just the database. On macOS/Linux the folder is owner-only (0700, files 0600). A from-source git checkout that already has ./data/state.db keeps using it. onememory doctor shows which directory is in use.

Upgrading from an early 1.0.x install (data kept inside the package folder): copy it yourself before removing the old package, e.g. mkdir -p ~/.onememory && cp -Rn "$(npm root -g)/onememory-mcp/data/." ~/.onememory/ (Windows: %APPDATA%\npm\node_modules\onememory-mcp\data\* to %USERPROFILE%\.onememory\), then run onememory rebuild-edges once.

cp .env.example .env     # set ONEMEMORY_TOKEN (openssl rand -hex 32)
docker compose up -d     # dashboard at http://localhost:8790
onememory backup --out ~/om-backup     # consistent hot copy; safe while the server runs

(The attestation signing key .attest-key is not copied; see docs/trust.md.)

onememory backup writes state.db (via SQLite VACUUM INTO) plus, if at-rest encryption is on, a copy of the .key file. Keep the key apart from the backup: a backup without its key is unreadable, and one stored next to it is not encrypted. See INFRA.md. The JSON route (admin token) also works:

TOKEN=$(cat ~/.onememory/.token)
curl -H "Authorization: Bearer $TOKEN" localhost:8790/export > backup.json   # or: onememory export --format json --out backup.json
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  --data-binary @backup.json localhost:8790/import                           # admin scope only
  • Restore into an empty store.POST /import does not de-duplicate: importing a backup into a store that already holds those notes creates duplicates.

  • Backups are snapshots of current notes, not per-edit history. OneMemory keeps only the latest version of each note (therev counter tracks edits; it is not a snapshot list), so a store with months of edits exports as one entry per note with its latest body — export time and size track your note count, not how often you rewrote them. The change feed's per-edit log is metadata only (titles, no bodies), pruned after 90 days, and is not part of a JSON backup.

  • Exports stream.GET /export andonememory export --format json page through the store and write as they go, so peak memory is one page (500 notes) regardless of store size — a multi-month store dumps in one pass.

  • Restores keep the history that exists.created_at ,updated_at ,source and therev counter all survive the round trip (a note at rev 26 stays at rev 26, soif_rev guards and the change feed stay accurate). Imports over 200 notes land in seconds and defer the O(n²) similarity-graph rebuild — the response says so and search works immediately; runonememory rebuild-edges when you want neighbor ranking back. Restored notes are re-embedded automatically when semantic search is enabled.

  • Markdown (one file per note, git-friendly) is a separate path: onememory export --format md --dir ~/memory — seedocs/file-sync.md .

  • Small footprint. One Node process, one SQLite file,2 direct runtime dependencies (@modelcontextprotocol/sdk ,zod ; ~94 packages installed in total, mostly the MCP SDK's own), no native modules, no vector database, no Python. Back it up by copying one file.

  • Change feed.memory_since /GET /changes : "what changed while I was away" — created, updatedand deleted notes grouped by source, with a per-device "since I last looked" marker. Driven by a local change log, so restores, file-sync imports and deletes are never missed. →docs/change-feed.md

  • Live wait.GET /changes?wait=25 /memory_since { wait_seconds: 25 } parks until something changes in your scope, so a session sees an idea added from your phone within moments instead of on its next poll.Use 25, not the 55 maximum: many proxies and tunnels cut an idle response at 30s (a 55s wait behind one died at 30.0s). Bounded (16 per token), and a dropped client or a server shutdown frees its slot;ack_mode: "manual" makes delivery at-least-once. →docs/change-feed.md

  • Task board, handoffs, catch-up.task_put /task_get /task_list /task_claim ,session_handoff , and onecatch_up call (changes since you last looked + open tasks + latest handoff). Built on plain notes, so it is encrypted, namespaced and searchable like the rest;onememory recall includes open tasks and the latest handoff at session start. →docs/tasks.md

  • Memory linter. Every write is scanned for secrets (API keys, JWTs, private keys, high-entropy tokens) and prompt-injection instructions ("ignore previous instructions", chat-template markers, tool-call coercion). Secrets are blocked; injection is quarantined for admin review and invisible to search and recall until approved. It is a pattern linter, with unit tests but no published false-positive rate: expect misses and the occasional false flag. →docs/memory-linter.md

  • Plain-file sync.onememory export --format md --dir writes one Markdown file per note with YAML front-matter, deterministic and git-friendly;onememory import --dir merges edits back three-way — newer wins, conflicts reported and the losing version kept, never silently lost. Works across machines through GitHub or Syncthing. →docs/file-sync.md

  • Scoped tokens.read /write /admin bearer tokens, enforced identically on REST and MCP — give your phone a read token and a leaked token can't wipe the store.

  • MCP and a plain REST API on the same store, plus a small dashboard.

Also included: optional semantic / hybrid search with your own embeddings endpoint (Ollama or any OpenAI-compatible API), natural-language time filters ("yesterday evening"), staleness warnings, near-duplicate consolidation, a similarity graph, access-based ranking, server-side namespace isolation, and a Claude Code SessionStart recall hook. → docs/search.md

flowchart LR
    subgraph Agents["Your agents"]
        CC["Claude Code"]
        CD["Claude Desktop"]
        PH["Phone / Web"]
        CU["Cursor / Copilot"]
    end
    subgraph OM["OneMemory (one Node process)"]
        MCP["MCP server\n(stdio + HTTP)"]
        REST["REST API + Dashboard"]
        LINT["Memory linter\n(secrets + injection scan)"]
        DB[("SQLite\n(one file)")]
    end
    CC <--> MCP
    CD <--> MCP
    CU <--> MCP
    PH <-->|tunnel| REST
    MCP --> LINT --> DB
    REST --> LINT

Write once from your desktop, read back from your phone — every agent shares the same store.

Other good projects solve overlapping problems, and we have not verified a feature-by-feature comparison against their current releases, so we do not publish one. Read their own docs and pick on your needs:

What OneMemory itself provides (all of it checkable in this repo): MIT license, Node 24+ with 2 direct runtime dependencies, one SQLite file, MCP over stdio and HTTP plus a REST API, optional semantic search with your own embeddings endpoint, a change feed that includes deletes, a write-time secret and injection linter, Markdown export and three-way import, scoped tokens with optional per-token namespaces, and optional AES-256-GCM encryption of note bodies. See SECURITY.md for what it does not do.

Tool Scope
state_write /state_read /state_delete write / read / admin CRUD with compare-and-swap ( if_rev ) and provenance (source )
state_search /state_relevant /state_list read keyword, semantic or hybrid search; vague-message recall; listing
memory_since read change feed ( wait_seconds = live wait)
task_put /task_list /task_get write / read / read shared task board across devices and models; client_key makes a create retry-safe
task_claim write take / heartbeat / release a task with a lease (compare-and-swap)
task_approve admin approve a command task so the PC's runner may run it ( RUNNER.md )
runner_status read is the PC's runner listening, and how many approved commands are waiting for it ( onememory runner-status ,GET /runner/status ;RUNNER.md )
session_handoff /catch_up write / read leave a handoff; one-call catch-up at session start
memory_ack read confirm a manual-ack change-feed page
memory_stats read totals, most-read, never-read, stale
memory_quarantine admin review linter-held writes
trust_policy admin list / change the per-namespace distrust list and the trusted attester keys ( docs/trust.md )
memory_consolidation_candidates /memory_consolidate /memory_rebuild_edges admin dedup and similarity-graph maintenance

REST mirrors them (/notes, /search, /relevant, /changes, /tasks, /handoffs, /catch-up, /stats, /export, /import, …). Full reference: docs/architecture.md.

onememory start | mcp | doctor | init
onememory changes --since last --consumer laptop
onememory runner-status                           # is each PC's runner listening? commands waiting?
onememory trust [list | distrust <w> | undistrust <w> | identity | attester-add <key> --server-id <id> | attester-remove <id> | attester-revoke <id>]
onememory trust [export-key --out <file> | rotate]         # EXPERIMENTAL: signed attestations (docs/trust.md)
onememory watch --consumer pc [--exec program args...]     # react to changes as they land
onememory backup [--out dir] [--include-approval-key] [--include-attest-key]   # hot copy of the database (+ keys)
onememory export --format md --dir ~/memory     onememory import --dir ~/memory
onememory export --format json [--attest] --out a.json     onememory import --file a.json [--require-attestation]
onememory quarantine [list | approve <id> | reject <id>]
onememory consolidate | rebuild-edges | reindex | relevant "…" | recall

onememory help explains each. Upgrading an existing 1.3.x install: docs/upgrading.md. The CLI runs with full local trust (no token).

Every note carries a server-stamped written_by (who last changed its title or body; a tag edit or a task status change does not count) and created_by (the first). It is the label of the authenticated token that made the call, or a fixed marker for writers with no token: stdio (local MCP), cli (an onememory command), server (its own jobs), runner (the command runner). A client cannot set it: written_by in a request body is ignored, and an import stamps the importing admin. The old source field is still there, but it is only what the writer claims, so anyone can put anything in it.

A note written by Claude Code and one written by Cursor are therefore no longer identical on read-back. MCP results add a "who wrote this" block under the untrusted-data banner, the session-start recall names the writer, and state_list, state_search and memory_since take written_by / not_written_by (REST: ?written_by=) so an agent can ignore its own writes or distrust a client. Rows from before 1.3.3 read "" (unknown). Details and limits: docs/provenance.md.

Trust (1.4.0, hardened in 1.4.1). writer_id (<server_id>/<label>) says which server a label came from. An admin can put a writer on a per-namespace distrust list (onememory trust distrust <writer> --namespace work, the trust_policy MCP tool, /trust REST): its notes, tasks, handoffs and feed entries are then left out of every read path unless an admin passes include_distrusted, they stay hidden after anyone else edits them, and a distrusted command task is never run. A JSON export made with --attest (?attest=1; the default export is the plain {"notes": [...]} as in 1.3.x) carries a per-note Ed25519 attestation; an import that trusts the exporting server's key keeps the original author as attested_writer, otherwise it flattens provenance as before and marks the note imported_unverified. (Attestations are EXPERIMENTAL in 1.4.x.) What that proves and what it does not: docs/trust.md.

Loopback by default and fail-closed: it refuses to bind a public interface without an explicit ONEMEMORY_TOKEN. Constant-time bearer auth on every data route, scoped tokens, DNS-rebinding Origin checks, per-client rate limits, size caps, an append-only audit log without note content, the write-time linter above, and an untrusted-data banner on every MCP read (REST responses do not carry it) so agents treat note content as data, not instructions. Optional AES-256-GCM encryption at rest for note bodies only (onememory encrypt enable; titles, tags, timestamps and embeddings stay readable), per-note TTL with reversible soft-archive, and ?explain=1 so a caller can see why a memory was retrieved. Per-token namespace permissions pin a token to one namespace so a single shared bridge stays isolated per agent — a pinned admin token is still confined. Your notes stay in one file on your machine; a tunnel only carries requests. Details and known limits: SECURITY.md, docs/token-namespaces.md, and docs/encryption-retention.md.

About the retrieval eval. evals/cross-device-tasks.json is 10 tasks over 10 notes and lexically easy; its perfect score is a regression gate in CI, not evidence of retrieval quality. Keyword-only state_search scores 0.70 recall@1 on it. See docs/encryption-retention.md.

| INSTALL.md | every MCP client | | INFRA.md | tunnels, phone/web access, n8n | | docs/configuration.md | all environment variables, server-side namespace resolution | | docs/search.md | semantic, time-aware and relevant search; staleness; consolidation | | docs/change-feed.md | memory_since /GET /changes | | docs/tasks.md | task board, claims, command tasks, handoffs, one-call catch-up | | docs/memory-linter.md | ONEMEMORY_LINT , detection rules, quarantine | | docs/file-sync.md | Markdown export/import, GitHub and Syncthing workflows | | docs/sessionstart-hook.md | automatic recall at Claude Code session start | | docs/encryption-retention.md | at-rest encryption, retention (TTL + archive), retrieval visibility | | docs/provenance.md | who wrote a note: server-stamped written_by , filters, what is and is not trusted | | docs/trust.md | distrust list, writer ids, signed attestations: what each does and does not prove, upgrade and rollback | | docs/token-namespaces.md | per-token namespace permissions — one shared bridge, isolated agents | | docs/architecture.md | modules, tools, REST API, schema | | SKILL.md | when and how an agent should use the memory | | CHANGELOG.md | release notes |

Several ideas were adapted from other open projects, with thanks:

  • llmem — consolidation candidates, the similarity-edge graph, access-count importance,#status staleness, and the context/relevant split with keyword extraction.
  • lontar (rymanalu, described in*"One memory across AI agents: what it actually takes"* on dev.to) — the fail-silent SessionStart recall hook, server-side namespace resolution from the working directory, and one-directionalreads_from grants.
  • lossless-memory — natural-language time phrases as a first-class query filter.

MIT — see LICENSE. Built by Dan Calas / Calas Automations Inc.

── more in #ai-agents 4 stories · sorted by recency
── more on @onememory 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/onememory-self-hoste…] indexed:0 read:13min 2026-10-25 · —