Skip to content

Private, encrypted memories

You can encrypt a private memory so that its content is readable only by you. The encryption key is derived from a passphrase that the server never stores — so a database dump, a backup, or the platform operator's own database access reveals none of your encrypted content.

This trades convenience for a real guarantee. Read the whole page before you encrypt anything: the promise is precise, and so are the costs.

A lost passphrase means permanently lost content

There is no recovery, no reset, and no admin override. If you forget the passphrase, the content encrypted under it is gone for good — the platform cannot help, because it never held the key. Write the passphrase down (a password manager is ideal) before you encrypt.

What the encryption does — and does not — protect

It protects your content at rest and against operator access. Node content, abstracts, and structured data are stored as ciphertext (AES-256-GCM under a key derived from your passphrase). Anyone who reaches the database — a dump, a stolen backup, a curious or compromised operator with query access — sees ciphertext, not your notes.

It is not end-to-end encryption against a hostile running server. While you have a memory unlocked, your passphrase has transited TLS to the server and the key lives in the server's memory for the duration of your session — that's what lets the server decrypt for you. A compromised or malicious live server could observe keys in memory during that window. So this is at-rest + operator-access protection, not a guarantee against a server that is actively hostile while you're using it.

private, not personal

The encryptable class is private — a memory you create and own, intended for material only you should read. Throughout this page, "private memory" means a class = private memory.

Don't confuse private with personal

Hadron also has a personal class — the per-user memories an App provisions automatically (chat history, preferences) described in Understanding memory. Despite the name overlap ("personal" memories hold your private notes), the personal class is not user-encryptable. This feature is about the private class only.

What is encrypted, and what stays readable

Some fields stay plaintext by design — the tree, search, and routing need them to function:

Encrypted Stays plaintext (by design)
node content, abstract, structured data node name, loc, alias, description, tags, properties; the memory's name and URN; edge names

The implication: someone with database access can see your node titles and structure, just not their contents. Title sensitive nodes accordingly — put nothing you need hidden into a node's name, description, or tags.

Encrypt a private memory

Encrypting is a one-time conversion of an existing private memory, done by its owner via the GraphQL encryptMemory mutation. It converts every existing node's content/abstract/data to ciphertext.

mutation EncryptMyMemory {
  encryptMemory(memoryId: "<memoryId>", passphrase: "correct horse battery staple") {
    id
    isEncrypted
  }
}
  • Owner-only, and the memory must be class = private (otherwise the mutation is rejected).
  • Supply exactly one of passphrase (the normal path) or a raw base64 32-byte dataKey (for advanced users managing their own key material).

Avoid a plaintext window

Encrypting an existing memory converts what's already there, but that content existed as plaintext until you ran the mutation. To keep content from ever landing unencrypted: create the empty private memory, encrypt it, then add content. Every write after that is encrypted on the way in.

Work with it from an AI agent (MCP)

This is the flagship flow, and it's fully shipped. An encrypted memory is locked by default in every MCP session:

  • Locked reads return a marker in place of the content — Locked: encrypted memory — call hadron_unlock_memory to read/write its content (spec 041) — with content and abstract blanked. Nothing leaks.
  • Locked writes are rejected. Nothing plaintext lands.

Unlock it for the current session, work normally, then lock it (or just let the session end):

  1. hadron_unlock_memory(memoryUrn, passphrase | dataKey) — owner-only; unlocks the memory for this session only. Supply exactly one of passphrase or dataKey. A wrong passphrase returns a clear error and changes nothing.
  2. While unlocked: reads return plaintext, writes encrypt automatically, semantic search (hadron_find_nodes) works over the decrypted content, and staleness markers behave normally.
  3. hadron_lock_memory(memoryUrn) re-locks immediately. Otherwise the key is dropped automatically when the session ends.

The key lives only in the server's memory for the life of the session — it is never written to disk and never shared with another session. Two people (or two of your own sessions) each unlock independently.

Scripts and the API (GraphQL)

For non-interactive callers, unlock per request with the x-hadron-memory-keys header on any GraphQL call. It carries a JSON array of { memory, passphrase } (or { memory, dataKey }) entries:

POST /graphql
Authorization: Bearer <your-user-token>
x-hadron-memory-keys: [{"memory":"acme.com::journal","passphrase":"correct horse battery staple"}]
Content-Type: application/json
  • Authenticated owner only, and at most 8 entries per request.
  • A wrong passphrase fails the request up front with WRONG_KEY, before any resolver runs.
  • Keys are request-scoped — nothing persists between calls. Each request that touches the encrypted memory carries the header again.

Portal and CLI

A point-and-click portal flow and a hadron CLI command are not shipped yet. Use the MCP tools (from an AI agent) or the GraphQL surface above for now. This section will grow when that UX lands.

Consequences to plan for

These are accepted trade-offs of the guarantee, not bugs:

  • Keyword search can't see encrypted content. It still matches on the plaintext fields (names, tags), but never the body of an encrypted node — even when the memory is unlocked. Keyword search runs over the stored columns, and the body is stored as ciphertext; unlocking decrypts results for display, not for the full-text index. (Semantic search is the exception — see the vector-index opt-in below.)
  • Semantic (vector) search requires an explicit opt-in. Embeddings are stored unencrypted and can approximately reconstruct source text, so indexing an encrypted memory is gated behind an acknowledgment of that risk. See RAG vector index → Encrypted memories.
  • Headless and scheduled agent runs are unavailable on encrypted memories — a run has no interactive session in which to unlock, and the server holds no key. Triggering one fails with ENCRYPTED_MEMORY_UNSUPPORTED. See Headless runs.
  • Git-mirroring writes plaintext to your own repo. If you sync the memory to a Git repo, content you write while unlocked is mirrored as plaintext into that repo — which is your repo. Nothing leaves your control, but don't expect the mirror to be encrypted.
  • Version history stores ciphertext. Prior versions are protected the same way the live content is.

Troubleshooting

If a call fails, the error code tells you what happened:

Code Meaning
WRONG_KEY The passphrase (or dataKey) doesn't match what the memory was encrypted with.
BAD_UNLOCK_INPUT You supplied both passphrase and dataKey, or neither, or a malformed x-hadron-memory-keys header.
NOT_ENCRYPTED You tried to unlock a memory that isn't encrypted.
NOT_FOUND The memory doesn't exist — or you don't own it (the two are deliberately indistinguishable, so ownership can't be probed).
SESSION_EXPIRED A write was attempted with no key staged for the memory in this session/request. Unlock again.
ENCRYPTED_MEMORY_UNSUPPORTED A headless run was aimed at an encrypted memory. Not supported — run interactively instead.