The core claim
No Myelin server holds any key material at rest — including the hosted one.
Brains, snapshots, and archived changes are all AES-256-GCM ciphertext under a key that is generated on your device, kept in your OS keychain, and never stored on the server in any form.
The server holds a 16-byte verifier — HKDF(DEK, "key-check") — so it can
reject a wrong key quickly instead of failing halfway through a decrypt. That
verifier is one-way. It tells you nothing about the key and nothing about the
brain.
A key reaches the server only as a request header, only over TLS, and lives only in process memory for a 10-minute idle window before being zeroed. This is why creating a brain requires the CLI on your own machine even when you are using the hosted instance: there is nothing on the server that could generate a key for you, by construction.
What that seals. Every at-rest window: disk theft, a stolen backup, a snapshot of the volume, an idle server, a decommissioned disk, a hosting provider with block-storage access. In all of those, an attacker gets ciphertext and nothing else.
And pull is always available. myelin pull brings the entire brain back
to a local machine — ciphertext plus snapshots, decrypted on your device. The
cloud is a location, never a cage. Nothing about a server copy is load-bearing
for your ability to keep using what you have.
The residual risk, stated plainly
A root attacker on the server, during an active session, can read that session’s brain out of RAM.
While a brain is open it is decrypted, and its key is in the server process’s memory. Anyone who is root on that machine at that moment can read both. This is the honest consequence of not using a confidential VM, and it is accepted rather than argued away.
What narrows it:
- Brains close after 10 minutes idle. Plaintext and key are wiped on close.
- Plaintext lives in
/dev/shm— RAM, never disk.PrivateTmp=yesgives the service its own, invisible to other processes and gone on stop. LimitCORE=0, because a core dump would contain decrypted brains and live keys.- The systemd unit drops all capabilities, forbids new privileges, blocks write-and-execute memory, and permits only loopback networking.
What does not narrow it: nothing here defends against an attacker who is already root while you are actively using the service. If that is unacceptable for your data, the answer is not a configuration change — it is not putting that data on a shared machine.
Zeroing, and Go’s garbage collector
Myelin overwrites key bytes when it is done with them. Go’s garbage collector may have copied those bytes elsewhere during their lifetime, and Go gives no way to find or wipe the copies.
Zeroing narrows the window. It does not close it. Anyone claiming otherwise about a garbage-collected language is overselling.
Key loss is final
Your brain’s key exists in exactly two places: your OS keychain, and the 24-word recovery phrase printed when the brain was created.
Lose both and the brain is gone. Permanently. By anyone.
There is no reset link, no support path, and no vendor override. The server has never held the key and cannot recover what it cannot read. This is not a policy that could be relaxed for a sympathetic case; it is a property of the construction, and it is the same property that makes the paragraph above true.
Write the recovery phrase on paper. Not in the brain. Not in a note synced to the same laptop. Not in the password manager whose master key lives on the same device.
The connector trade-off
claude mcp add --transport http with an X-Myelin-Key header lets you reach a
brain from a browser or a phone. It also means that platform now holds your
key, in whatever way it stores connector headers.
This is a real reduction in the guarantee above, and it is offered anyway because browser access is genuinely useful and the alternative is not having it. Decide deliberately:
- On a laptop, prefer stdio (
myelin serve). The key never leaves the machine and there is no reason to accept the trade. - For remote access, consider a brain scoped to what you would not mind a
platform holding, and keep genuinely sensitive material in a
local_onlybrain that never leaves your machine at all.
local_only
A memory with sensitivity='local_only':
- is excluded from every HTTP read path — recall, get, list, edge expansion, and the core-tier prepend;
- cannot be written over HTTP at all;
- is never pushed to the server. A brain that mixes sensitivities is uploaded as a filtered export-import rather than a file copy, because deleting rows after copying would leave them recoverable in SQLite’s free list;
- has its edges dropped when it is withheld. An edge pointing at a memory that is not there would itself reveal that something was withheld.
Secret refusal
Content that looks like a credential is refused before it reaches the database: seventeen structured rules plus entropy checks. Refusals name the rule and the byte offset, never the matched text, so a refusal is safe to log.
This is a guard against accident, not a guarantee against determination. Someone who wants to store a secret in Myelin can obfuscate it past the scanner. What it stops is the ordinary case: pasting a block of config that happens to contain a key.
Audit
Audit rows carry a timestamp, tenant, token, brain, endpoint, outcome class, and latency. They carry no memory content, no query text, no payloads, and no key material — the table has nowhere to put them, and a test asserts the live schema against the permitted field set, so a future column fails the build rather than leaking quietly.
purge and shred
purge hard-deletes one memory: the row, its full-text index entry, and its
edges. It leaves a tombstone recording that an id existed, so doctor can
verify the deletion was complete. It is the only exception to append-only
memory, it is CLI-and-admin only, and it is deliberately not an MCP tool.
shred destroys a brain on the server: ciphertext, every snapshot, and the
archived changes. Combined with destroying your key, nothing is recoverable from
any copy anywhere — including copies an attacker took earlier. That is what
makes it a crypto-shred rather than a deletion.
Note the ordering. Shredding the server copy while keeping your key means you can still read anything you pulled. Destroying the key is the irreversible half, and the CLI asks about it separately for exactly that reason.
Authentication
- PATs are the workhorse: 256-bit secrets stored as argon2id hashes, scoped over actions (read ⊂ write ⊂ admin) and optionally over a single brain. Revocation takes effect on the next request — nothing caches the lookup — and revoking a token also evicts any key cached under it.
- JWTs, when a host requires OAuth: HS256 only, 15 minutes, nothing else
accepted.
alg: noneis rejected. - OAuth 2.1 PKCE: S256 only.
plainoffers no protection against an intercepted authorisation code, which is the entire point of PKCE. - Magic links: single-use, 15 minutes, held in memory only.
Transport
TLS is mandatory for anything carrying a key. Myelin refuses to bind a
non-loopback address unless MYELIN_TRUST_PROXY=1 says a TLS terminator is in
front, and it returns 426 Upgrade Required rather than accepting a key
header over a plaintext connection.
What Myelin does not defend against
Stated so nobody has to infer it:
- A compromised client device. If your laptop is owned, your keychain is owned.
- A malicious or compromised MCP host. Whatever the host can ask for, it gets.
- Root on the server during an active session, as above. On the hosted instance that means the project owner, who has root on the box. If that is not acceptable for your data, run it yourself — the runbook is published for exactly that reason.
- Traffic analysis. The sizes and timings of requests are visible to anything on the path, even though contents are not.
- A user determined to store a secret in a memory.
- Denial of service beyond simple rate and size limits. Quota bounds consumption; it does not stop a determined flood.
Reporting
Open a GitHub issue for anything low-risk, or contact the owner directly for anything that should not be public first.