The command line and the MCP server, in full
This is the reference for @mnema/code: every surface the package gives you, what each one
promises and what it leaves unproven. The package’s README is the short
version, and the install page has the steps.
It holds no domain logic of its own: it resolves where you are, calls one function in the
packages below it, and prints what came back. The workflow rules, the projections, and the
proof live in @mnema/core and @mnema/chain; keeping the surfaces thin is what makes the
CLI and the MCP tools behave identically, because they are the same call.
What it gives you
Section titled “What it gives you”- An MCP server over stdio: an agent host connects and the agent records its own work: tasks, decisions, skills, memories, observations, handoffs. Every write is attributed to the identity that signed it and pinned to the session it happened in.
- A CLI (the human side of the same record): create and move work, capture
knowledge, read context, and verify the chain. It also holds the one write the
MCP server has no tool for:
mnema tail pruneauthorizes the cut of a whole tail, which is the only command here whose consequence is destructive. - Workflow gates: an illegal move is refused with a typed reason, on both surfaces, because both ask the same gate. Some moves require their evidence (a reason to cancel, a note to complete) and the gate enforces it.
- Three places to write: a committed project record the team shares, a
private one for this machine, and a global one for knowledge that outlives any
single project. What a fact IS decides which: a decision, a task, a skill and a
link are the project’s, so they go to the committed record whoever wrote them. A
handoff, and the brief recorded with it, goes where its task lives, so a private
task’s brief is not committed by a handoff that named no tree; it goes to the
committed record only when no tree here holds the task, and the reply says so. A memory and an observation are the open case (the kind cannot say
whether a note is the team’s or nobody’s but yours), so those two still follow the
author: an agent’s stays on its machine, yours goes to the record.
--scopeoverrides any of it, a move follows the entity it moves, and every write says which tree it landed in. - Reads that answer questions: where things stand (
status: the live work, the adopted patterns, the decisions in force, and what is still waiting on somebody to rule on it), where you left off and what is still in flight (resume: the last run, and the runs the actor has open), what the workflow allows next (task next), and whether a move would be allowed at all (task guard, a dry run that records nothing). - Audit reads: an entity’s history across trees (
timeline), what it is connected to (refs), who authorized what (audit accountability), recurring shapes like reopens and supersessions (audit antipatterns), what looks like a credential (audit exposure), and where each adopted pattern came from (skill provenance).diagramprints the three state machines, or an entity’s history or connections, as mermaid text for a page or an editor to draw. They report; they do not judge.audit antipatternsalso says whether the run that MOVED a pattern had been served its body, and answers not observable, never did not consult, for every move whose run recorded no reading at all, which is the state a move made by a person is always in. It is on this surface only, and the page says what it can witness. - A feed your SIEM can read:
mnema exportemits the record as OCSF Entity Management events, one JSON object per line, for Security Lake, Splunk, or anything else that speaks the schema. It carries the envelope only (who authorized what, over which entity, in which session, signed by which key) and never a payload, for the reasonmnema audit exposureexists. It writes to standard output and sends nothing anywhere; what it does and does not promise is in the table below. - A page of the record that verifies itself:
mnema sitewrites oneindex.htmlfrom the committed (public) tree, by default under.mnema/site/(the record’s own.gitignorekeeps it out of git, andmnema siteadds that line first to a record that lacks it);--out <dir>names another directory, and one inside the record and outsidesite/is refused. The page lists every decision as a card (its reasons, who recorded and who accepted it, as a person or as an agent, its history and the paths it is addressed at) with the public notes that belong to it, and a timeline; with JavaScript a filter opens on the decisions in force and narrows them by state, identity, agent or person, day, text and path, and the stored events of a tail are drawn when it is opened. The page carries the record’s files and runs the same verifier asmnema verifyover them in the reader’s browser, with no request and no key, and once the record verifies it derives the page’s lists again from those files with the product’s own code and names each difference from what the page shows. Nothing from a private or machine-global tree is in it, and no script or style comes from elsewhere. What the verdict does and does not prove is in the table below.--back-link <url>puts one link, labelled “Back”, to that address (anhttp(s)://address or a path on the same site) at the top of the page, for a page that sits inside a site; without it the page is the same bytes, and a//host, a backslash, anhttps:with no//or any other scheme is refused. - A brief an agent reads without being asked:
mnema briefprints the decisions in force and the adopted patterns as markdown, which the plugin below hands to every session, andmnema brief > MNEMA.mdputs in a file of its own. The redirection is yours and it replaces the whole of the file it names, so the example names a file nothing else claims; in Claude Code the line@MNEMA.mdin aCLAUDE.mdbrings that file in, where a sentence naming it is read only if the agent chooses to open it, and anAGENTS.mdis read there only where noCLAUDE.mdexists. It carries the committed record (what a clone gets, not what one machine keeps) and the document says so, because a file read as instruction is read as the whole of what governs. It is a projection: the record stays the thing with the proof, and the file can be thrown away and made again. The same record always prints the same bytes, which is what makesmnema brief | diff - MNEMA.mda staleness check. - A plugin for Claude Code, in
plugin/, that stops the delivery from depending on somebody remembering to regenerate a file. It declares fifteen hooks and the MCP server below, in one installation. As a session opens, one hook runsmnema briefand hands over the document, and another runsmnema recalland hands over the notes: the memories and observations recorded for the project, from every tree this machine holds, including the private one the document may not carry, the ones near what the session touches first; a project with no notes gets nothing from the second. APreToolUsehook onWrite|Edit|NotebookEdithands over the rules of the record addressed at the file about to be written, beside the result of that write (the ones still in force, each with the id you would cite) and hands over nothing for a file none of them addresses, which is why the opening document says how many of the project’s rules have an address. The two opening hooks are reads: they append nothing and open no run, so the record’schannel.servedcounts what the per-edit hook pushed and never the opening texts. It is mnema noting that it delivered the hook’s answer, not a person’s approval of anything. The per-edit one records that it served, and holds a write for a person only where a rule of your own record asks it to: the plugin’s page says how. Where another run of this machine’s identity was charged at that same path in the last 30 minutes (looking through each channel’s 2,000 newest charges, no further), a rule asked for a person or refused a write there, in a run still open, the reply ends with one sentence naming that run’s agent and how many minutes ago; it is read from the record and appends nothing, and a path no rule has ever charged anyone at gets none. The fourth is that same pause for a host whose hooks are processes: VS Code’s agent runs it asmnema before-a-write --host vscodebefore a write, and it asks, and records, exactly where the third would; Claude Code, Cursor and Qwen Code never run it. Beside them, on the samePreToolUsematcher, one more hook runs only where$CURSOR_VERSIONis set and$QWEN_PROJECT_DIRis not (a Qwen Code opened in a Cursor terminal inherits Cursor’s variable, and only Qwen Code’s refusal runs there): the refusal for Cursor’s command-line agent, which answers a rule that refuses a write and nothing else, since that agent ignores a pause. Another runs only where$QWEN_PROJECT_DIRis set: the same refusal for Qwen Code, which answers a rule that refuses a write and, where the write goes through, hands over the rules addressed at the paths it touches (mnema before-a-write --host qwen); Codex and OpenCode run the same verb under their own names and answer those same two things, recording that the rules were handed over once for each session of the host. Copilot CLI and Gemini CLI, whose hook before a write has no field for text, hand over the same rules from the hook that runs after it (mnema beside-a-write --host copilotorgemini): it asks the record whatbefore-a-writeasks and records the same onechannel.servedper session, but it never refuses or asks and records no refusal or asking. The fifth ismnema tally, run atStop: when the session’s own tool calls wrote a file in the last response, it prints one line saying how many files they wrote and how many decisions were recorded in the project since the session opened, counted off the transcript the host names and off the record: no model is called and nothing is recorded. The line goes out as the reply’ssystemMessage; which host shows it was not measured.mnema switch off session-tallystops it. The same command keeps the session journal (see The session journal), a small file on this machine that lets the nextStopread only what the host wrote since. The sixth runs only after a conversation was compacted (thecompactmatcher ofSessionStart) and hands it back what entered the record while its session ran, one sentence read from that journal (see After a compaction); nothing runs before a compaction, because the host throws away what a hook says then. The seventh runsmnema corrections, also atStop, and is off until you switch it on (mnema switch on user-corrections): it reads the transcript’s own words, the one reader here that does, finds the prompts that open by correcting the agent (“no, …”, “stop …”, “do not …”, “that is wrong”, “use … instead”, in English and Portuguese), and the answers the person gave to a question of the agent (the option chosen in the host’s question tool, or the prompt after a response that ended in a question mark, quoted with the question), and records each as aproposeddecision in this machine’s private tree, citing the session and the line, at most five at a time and never the same line twice. A match is a pattern and not a judgement, which is why it is proposed. No model is called. The eighth is aSubagentStophook that runsmnema handback: a subagent hands the decisions it settled back in its final reply, and the hook asks that reply to end with one fenced block, info stringmnema-handback, holding{"decisions":[…]}(the list empty where it settled nothing) in the schemamnema handback --schemaprints. A reply with no such block, or a block outside the schema, is sent back with the format as the reason (exit 2): once, since the host’s second stop of the same subagent is let through. It checks the shape and never whether the subagent settled something;mnema switch off subagent-handbackstops it. It is held in Claude Code, against the real binary; the other hosts that read the plugin’s hooks file were not measured on that event. The ninth is aPostToolUsehook onAskUserQuestionthat runsmnema settled: after a question the agent asked the person was answered, it hands the agent one sentence beside the answer, that a choice may have been settled there and thatrecord_decisionrecords it, and it records nothing and reads which tool ran, never the answer. A shell in front of it starts nothing for any other tool, since VS Code runs a plugin’s hooks whatever their matcher says. It is on untilmnema switch off choice-settled, and held in Claude Code against the real binary; the hooks files of Codex (request_user_input), Copilot CLI (AskUserQuestion), Gemini CLI (ask_user) and OpenCode (question) carry the same hook, documented and not measured. The tenth is aSubagentStarthook that runsmnema brief --hook: the subagent that begins is handed the same document the session opened with, under the event the host routes the reply by; it is the opening’s channel and the opening’s switch, somnema switch off brief-documentstops both. It is held in Claude Code, against the real binary; VS Code documents the event and was not measured on it. The eleventh is aPostToolUsehook onWrite|Edit, in Claude Code, that runsmnema after-a-writebehind a shell that reads the path out of the payload and starts nothing for a path that can be neither of its two kinds. A note of Claude Code’s own memory for this project (projects/<project>/memory/*.md,<project>named as the host names it after the main working tree, so a note in another project’s directory is never copied, under$CLAUDE_CONFIG_DIRor~/.claude, or the directory theautoMemoryDirectorysetting names in your own settings or the project’s.claude/settings.local.json) is copied as a memory into this machine’s private tree, with a marker naming the file and a hash of its content, so a note is copied once per content: this is on to begin with (mnema switch off host-memory-mirrorstops it), never copies the indexMEMORY.mdor a note oftype: user, and never writes to the public tree. A file that holds decisions (underdocs/adr/,docs/decisions/or anadr/directory, or aCLAUDE.md,AGENTS.md,GEMINI.md, host rules file, plan, handoff or ledger whose new lines open asRuling:,Decision:or a correction does) that the record cites nowhere gets one line beside the write naming the command that reads it, once per session and file; nothing is recorded for it, andmnema switch off decision-file-writtenstops it. TheStopline counts the notes copied since the session opened. The thirteenth is aPostToolUsehook on theAgenttool and the fourteenth aSessionEndhook, both runningmnema dispatched(see What a session handed to other agents): the first dispatch of a session records one private task, each dispatch a private handoff on it, and the end of the session closes it;mnema switch off agent-mirrorstops it. It is held in Claude Code on the payload the host measured to hand the hook, and not run against a model; the other hosts that read this hooks file were not measured on either event. All fifteen are silent where there is no project, so a machine that installs this and opens a session somewhere else sees nothing. Asserted intests/the-record-arrives-unasked.test.ts,tests/the-rule-reaches-the-writing.test.tsandtests/the-record-asks-for-a-person.test.ts, the fourth intests/a-host-that-runs-commands-asks-for-a-person.test.ts, the fifth and sixth intests/a-session-says-what-it-wrote.test.ts, the seventh intests/a-correction-becomes-a-proposal.test.ts, and the eighth intests/a-subagent-hands-back-its-decisions.test.tsandtests/host-contract/a-subagent-is-sent-back-for-its-handback.test.ts, and the ninth intests/a-settled-choice-is-said.test.tsandtests/host-contract/a-settled-choice-reaches-the-model.test.ts, and the eleventh intests/what-a-write-means-to-the-record.test.tsandtests/host-contract/a-note-of-the-hosts-memory-is-copied.test.ts; the plugin’s own page states what it carries and what it leaves behind. VS Code’s agent and Cursor’s command-line agent read the same plugin: the two opening hooks run in both, the rules at each edit are Claude Code’s, and the pause for a person reaches VS Code too and not Cursor’s agent, which runs the hook and ignores the pause: the plugin’s page has the table, host by host, and how each claim is held says which of it a test holds. Codex, GitHub Copilot CLI, OpenCode and Gemini CLI read files of their own in the same plugin folder: the two opening texts and the refusal, and in Copilot CLI the pause too, each held by a job against the host’s real binary; Hosts says how, host by host. Qwen Code installs the plugin itself, converted into an extension of its own, and runs the same hooks file: the refusal is a command in it that starts only whereQWEN_PROJECT_DIRis set. - A rules file for a host without the plugin:
mnema rules-file --host vscodeprints the committed rules in force whose address is a file or a directory, as a.instructions.mdwith anapplyTo,--host cursoras a.mdcwithglobs, and--host claudeas a.claude/rules/mnema.mdwith thepathslist Claude Code’s documentation names (not measured here). A directory is written asdir/**, which each of the three fields takes as a glob; it is not exact the way a file is: VS Code puts**/before a relative pattern, sosrc/billing/**would also match asrc/billinganywhere else under the folder it reads, Cursor matchesglobson its servers, where it could not be measured, and whether Claude Code’s**reaches a name that starts with a dot was not measured. The output says so beside the file. The project root is left out, and so is an address holding a character a glob reads as syntax:app/[id]as a glob matchesapp/i. The same**/reaches a file too (src/x.tsalso matchesother/src/x.ts) and the output says so where it prints a file for VS Code. Every rule it leaves out is named on the second stream with the reason, in the same run; the file goes to stdout, and where it lands is your choice: the>replaces the whole of the file it names. Asserted intests/a-rules-file-carries-only-what-becomes-a-glob-exactly.test.ts, against Node’s own glob matcher; what each host does with the pattern is not held yet. - A switch for everything it pushes:
mnema switchsays where each of those channels stands and what each carries;mnema switch off edit-rules-pushstops the per-edit push,mnema switch off brief-documentstops the opening document andmnema switch off recall-documentstops the notes beside it. Nothing arrives switched off. The switch is a fact of the record rather than a setting, and that is the whole design: turning something off is legitimate and turning it off in silence is not, so the switch is signed, attributed, dated and scoped like every other fact, and a later reader can tell “no rule addressed that file” from “somebody had turned the push off that week”. It defaults to the committed tree, so the team reads it and the opening document says the push is off instead of implying it had nothing to say;--scope privateis the switch that means this machine, and thenmnema switchis the only reading that can report it. It is on the CLI alone: the MCP server serves no tool for it, so an agent cannot switch off what governs its own work through the door built for agents. Asserted intests/the-switch-is-a-fact.test.ts.
What it proves, and what it does not
Section titled “What it proves, and what it does not”The record is tamper-evident, not tamper-proof: it does not stop an edit, and
mnema verify shows when what is in the record has changed since it was
signed, and a cut that leaves the last checkpoint in place; by default it does not catch a cut off the end that takes the last checkpoint with it, or one tail deleted whole while another remains (a key left without a tail is noted, not failed). A record in which no tail holds anything any more exits 1, by name, at every --require; --since <rev> exits 1 when files the revision held are gone. It reads the events and public keys of
this project’s trees (no private key, no network) and prints each verdict
verbatim. The surfaces never upgrade a verdict into a stronger claim.
| Claim | What actually holds |
|---|---|
verify passes |
Nothing verifiable is broken: the hash chain holds and every signature it found checks out. It is not a claim that every event is signed: the verdict names the level it reached (verified (T1/T2/T4), … up to the last checkpoint, or verified (T1 only): no signature was checked), and only the first of those means every event is covered. |
verify covered the record |
Both trees of the project: the committed one and this machine’s private one, each with its own verdict under its own name. The exit code is the weakest of them, so a gate cannot pass because one tree is healthy. It used to cover the committed tree alone, which said nothing about signed facts written --scope private. This machine’s global tree is a third record, shared by every project on the disk; mnema verify --global covers it, and nothing does by default. Each tree is a record of its own, and they are read together for one thing: an agent key one of them retired is retired in the others the command covers, so an event another tree holds, signed by that key with a later at, is a break of that tree and is not counted as attested. A retirement written in a tree it did not cover (another machine’s, or the global tree without --global) is not seen, the output does not say so, and the second reader and the page, which read one tree at a time, do not cross. key revoke --agent writes the trees one after the other, so what the agent signed in a tree between two of its retirements is refused in the tree retired later; and the cross compares the at the signing key wrote, so a leaked key that writes an earlier at passes it. |
verify covered every project |
Only the ones you named: mnema verify --workspace <path...> gives one verdict over the projects at those paths, and a bare verify covers the project you are standing in. The set is never searched for: nothing walks the disk looking for a .mnema/, so no verdict of yours covers a project you did not name. Two names of one record (the same path twice, a subdirectory of one already named, a symlink) count once; a named path with no record is reported, left out of the level, counted at the end, and makes the exit non-zero: naming a path is asserting that it is a project, so a typo in a CI step is news rather than a silent pass, and --allow-no-record is how a caller declares otherwise. The aggregate is the weakest project’s, so nothing passes on another project’s proof, and a set in which no path holds a record exits non-zero rather than reading as a pass over nothing. Costs one full replay per project. |
| A tree with no record | Reported as exactly that, and it moves neither the verdict nor the exit. The private tree never travels with a clone, so a fresh clone has none, and absent is not broken. |
| Events are signed | True up to the last checkpoint. Events written after it rest on the hash chain alone, and verify reports that count separately rather than folding it into a pass. A record with no verified checkpoint at all is reported as T1 only: the hash chain held and no signature was checked. |
| A tail is written by its own key | A tail id begins with the fingerprint of the key that owns it, and every checkpoint and every event of the tail, checkpointed or not, has to name that key as its signer (FORMAT.md section 6). An event above the last checkpoint that names the tail’s own key still stands as not yet signature-covered (FORMAT.md section 6.2): only one that names another key is refused. One that names another key, even a key enrolled for the same person, is refused as tail-signer-mismatch, in the same words by verify, the second reader and the page. A version from before this rule verifies such a record. |
| The record could be read | Part of the verdict, not an assumption. A stored line that will not parse is reported as an UNREADABLE issue naming the tail and the position: never a green over bytes nobody can interpret, and never a parser message with no address in it. Every such line is named, not only the first, and the other causes in that tail are named beside it as far as the missing line lets them be checked (the tail’s ownership proof, whose key signed it, each checkpoint’s own signature); across a missing event line the hash chain is not walked and no content root is folded, so what the hole would make look broken is not said as a cause. |
| A formatter rewrote the record | Still a refusal: the bytes on disk are not the bytes that were signed, and the exit, the level and every issue stay what they were. Beside them, one reformatted line names the files and how to put them back, and only when that is proved: read back in canonical form, in memory (nothing is copied or written), the whole record verifies with no issue, and every line that had to be put back is covered by a signature (an event by a verified checkpoint, a checkpoint or a tail proof by its own). A line whose canonical form is another value, a line whose own hash was recomputed, the last events of a tail that no checkpoint covers, and a reformat beside any other break never get the line. It is what prettier, Biome, dprint and pre-commit’s pretty-format-json leave of tailproof.json, and what an .editorconfig with end_of_line = crlf (or mixed-line-ending --fix=crlf) leaves on every file. Over a record nothing rewrote it costs nothing; over one it explains, it costs one more verification. The Python reader refuses the same records and does not say it. |
| An edit is caught | An edit made without the signing key is caught, because signatures cover a root recomputed from the event content. Someone holding the key can rewrite and re-sign: detecting that needs a witness outside this machine, and mnema witness stamp asks for one. What it attests is that a checkpoint EXISTED at an instant, so a chain rebuilt this morning cannot claim a history; it is opt-in, and a record nobody stamped reads not covered exactly as every record did before. |
| Nothing was deleted | Still not proven locally, and the sentence that used to stop there was true for a reason worth keeping: a hash chain shows what changed, never what was removed, and a tail deleted together with its key, while another remains, leaves nothing on disk to cross (a record left with no tail holding anything is refused by name). What is new is the other half: the record can now NAME a cut, so an unexplained absence and an authorized one are no longer the same silence. Committing the record to git is still what preserves the files a deletion would take with it. |
| A cut can be told from a tampering | When it was authorized in advance, and for a WHOLE tail. A tail.pruned written while the tail is still there records which tail, how many events it held and the head it held them through (all three checked against the disk before the fact is signed) and verify then reports that account instead of listing the three reasons a key might have no tail. Cutting PART of a tail is not covered: removing a line from the middle of a tail stays loud, and so does one off the end while the last checkpoint stays, but a cut off the end that takes the last checkpoint with its events reads verified (T1/T2/T4), as the record before it did. A waiver is not permission (anyone who can write can sign one, and it names who did), it is not a cure (a tail that is present and broken keeps every issue it had), and it does not remove anything: once the record is pushed, the events are in every clone and on the remote. mnema tail prune is what writes one, on the CLI alone; the cut itself stays yours to make. |
| Gates protect the record | They protect its shape, not its contents. A gate refuses an illegal transition; it is not access control. Anyone who can run the CLI writes as this machine’s identity. |
| Every move on the record followed the workflow | Every move a reader applies did, and a green verify names any move that broke the workflow. A gate judges a move on its subject as the chain has it (the lines of the record that name the subject, read off the segments) and not on the projection kept beside the record, which nothing signs: an edited projection does not get a refused move written. A move written by hand with a valid key that the workflow does not allow stays on the record, but no reader applies it, and verify names it in its census without failing (the workflows are part of the format, FORMAT.md §6.7). The second reader beside the format judges every move by the same tables and names the same moves. A binary from before applies every move and names none. |
| A lost key can be restored | Only from the backup key mnema init makes, and only where the record proves that key a member: the committed project tree. mnema key restore is that path: local, offline, no service to ask, because anything able to hand your identity back could forge it. The private and global trees are born knowing one key, so a lost key cannot be replaced in them; they are uncommitted, so the disk that takes the key takes them anyway. It restores a person’s key and refuses an agent’s, which mnema key restore --agent <name> <file> puts in that agent’s folder instead. |
| Your machines are one author | True for machines the record proves belong to one identity, which is what enrolling a second machine records. A machine nobody vouched for writes as a different identity, honestly and permanently; that is not a bug to fix later, it is what an unvouched key means, and the write that founds it says so, once, naming the identities already there, while the public trees of your other projects can still be spared: by an enrollment made inside each of them, from a machine already in that identity, and pulled before the new key writes there, never in the project that split, where an enrollment alone joins nothing. mnema audit accountability names it afterwards, in --json too. When the record proves a key belongs to two identities, no command picks one for you: the write is refused, and the refusal names the ways out the record shows: the identity that should not have the key retires it (mnema key revoke <fingerprint> --reason "<why>" inside the project, from a machine that writes as that identity, then commit and share the record), and the key speaks for the other from then on. Where the key is that identity’s last (as a key that founded an identity by writing is), another key joins it first, from the checkout that still writes as it: pull the record, mnema key enroll <the line> (where the other key is a copy this machine keeps, such as the backup key an init made, its line comes from mnema key request --anchor <that identity> --key "<its file>"), the revocation, then mnema key restore "<the key file>" in that checkout, which still records the identity the key has left, and until the restore, every write to the public tree there is refused rather than signed with a key that identity no longer counts. Where the record shows the key writing as neither identity, it names no checkout that could separate them, and the refusal says so rather than promise. |
| What a run cost | Not proven at all, and mnema usage says so on its own last line. The record holds no cost, deliberately: the number lives in Claude Code’s transcripts, which the host deletes on a retention it decides, so a cost recorded in the chain would be a signed figure whose only witness is gone in weeks. So the verb crosses the two readings when you ask, reports tokens and a model id, never dollars, and never a price table, and names the host session it read so you can check the same file. Which session belongs to which run is that command’s inference from two clocks, not a fact the record states: one session in a run’s window is attributed, more than one is named and not attributed, and none says no transcript rather than 0. |
| How often an agent used a tool | Only what this machine’s MCP server answered. mnema usage --tools counts, per tool, the calls the server on this machine answered for this project, from a file kept outside the record: nothing is signed, verify does not read it, and the chain has no field for it. A call another host or another machine answered is not in it, nor is any use of mnema outside the MCP (the command line, a hook), nor a call made while tool-call-count was switched off or before the machine had a key of its own. It holds the tool’s name, whether the tool writes, the instant and ok or the refusal’s code, never an argument or a word an agent wrote. |
A page’s verdict is mnema verify’s |
For what it says: the page runs the chain’s own verifier over the public tree’s files it carries, in the browser, and prints the verifier’s own sentence: the same one mnema verify prints in a fresh clone of the repository. It differs from the author’s machine by at most one clause, the census note about a backup key, which only a machine holding that key can word. It checks the hash chain and every checkpoint signature against the public keys carried with the files, and nothing else: it does not say a key belongs to the person a list names, and the chain’s verdict does not cover the lists. mnema site wrote the lists from the files, and once the record verifies the page derives them again in the browser, from the files it verified, with the same TypeScript the command used (bundled into the page), and names each difference from what it shows, or says match; over a record that did not verify it says the lists were not checked. That is not a second, independent reader: it is the product’s own code run twice (the Python reader verifies the chain and does not derive decisions), it reads no style, and a reader who is handed the page by the party being checked is trusting the script the page carries; the check is worth most when the page is rebuilt from a clone. It publishes every word of the committed tree (rationales, notes, raw events), so what is in git is on the page, and a repository that must not be read should not publish one. |
| An exported skill is what the record proves | The BODY is, byte for byte: mnema skill export writes the recorded text verbatim, and nothing summarizes, reformats or improves it. The description beside it is not signed and is not in the record: the skills specification requires one, the chain has no field for it, so it is derived at export time by a stated rule (the first sentence of the body, cut to 1024 characters) or given with --description. No model produces it. Only an adopted pattern leaves (a proposal dropped into a host’s skills directory is read as how the work is done here, and a deprecated one wears a live one’s face) and there is no --force. The metadata carries the record id and the whole identity that adopted it, so a third party with the repository can check the line with mnema show and mnema verify. Nothing comes back the other way: there is no import, because a SKILL.md from elsewhere would enter as a body signed by us asserting a provenance we do not have. |
| An exported audit feed is the record | It is a projection of it, and it is not the proof. mnema export emits one OCSF Entity Management event per line for a SIEM, and it carries the envelope only: when, which operation, who authorized it, which agent executed it, in which session, over which entity, signed by which key. No payload of any kind leaves: not a memory’s text, not a decision’s rationale, not an observation’s body. The reason is the row below: the record holds credentials mnema does not recognize, and a feed carrying bodies would push them off this machine into somebody’s search index, permanently. A line that is altered in transit is not detectable by the SIEM: the signature in the record covers mnema’s own canonical bytes, not this projection, so nothing here is an attestation and OCSF’s record_integrity profile is deliberately not used. What each line does carry is enough to find the fact back in the record (the subject, the original instant, and the tree), so the answer to is this line real is a question you ask mnema show and mnema verify, never the index. Nothing in mnema ever reads a feed back, and the verb sends nothing anywhere: it writes to standard output and whoever forwards it decides the rest. |
| Credentials stay out of the record | Only the ones mnema recognizes, and only where the value does not read as a name you chose. A value in a known format (a cloud key, an API token, a PEM private key, a password inside a URL) never reaches the chain, and which of two things happens depends on the field. In a body (a memory’s text, a decision’s reasoning, a note) it is replaced with a typed placeholder and the reply names what was replaced: the fact survives the redaction. In a name (a title, a skill’s name, an agent, a run, either end of a link) the whole write is refused and nothing is recorded, because a name with a placeholder in it is not that thing redacted, it is a different thing under its id, permanently. A proprietary token, a password written out in prose, a base64 blob: those are written verbatim, and nothing deletes a fact afterwards. It reduces the damage; it does not make the record safe to paste secrets into. |
| Email addresses stay out of a body | An email address in a body is replaced with <email> and the reply says so; the mnid already says who wrote, so the record has no use for it. Left alone: the user of a URL (https://user@host), an scp-style git remote, a noreply address (the kind a commit cited from git carries), and anything in a name, which is never rewritten. It reduces the damage; it does not find a person’s name, a phone number or an address written another way. |
The honest summary: local cryptography covers alteration; the history a git
remote keeps covers omission and gives the signing key a history someone else can
check; an outside witness dates the record. This summary used to hand all three
to “an external witness”, and the list below it said that the one this product wires
in (mnema witness) does exactly one of them. It does:
- It dates.
mnema witness stampasks the public OpenTimestamps calendars to attest the digest of a checkpoint’s signed message;mnema witness upgradegoes back for it once a Bitcoin block carries it.verifythen reads the answer off the disk (never off a network) and reachesexternally-witnessed. Only the hash of that digest, joined to a random nonce, leaves this machine: no id, no title, no body, no count. Between the ask and the block the verdict saysPENDING, which is not coverage. - It does not cover omission, and it does not tie the record to an identity. Committing the chain to a shared git remote is still the path for both, because git preserves what a deletion would erase and gives the signing key a history someone else can check.
A record nobody stamps is unchanged in every byte: nothing on the writing path asks anybody for anything, so a checkpoint cannot fail because a calendar was down.
Install
Section titled “Install”@mnema/code is on npm, and this is the install (the install page has the steps for a Node
installed by the system, and Building it from source runs the main branch):
npm i -g @mnema/code# or, if your global binaries live under pnpm:pnpm add -g @mnema/codeEither puts the mnema binary on your PATH. Requires Node 24.15.0 or a later 24, or 26.0.0 or later; the package is
ESM-only, and it brings the other four with it. npm view @mnema/code version says which version the
registry serves.
From the terminal
Section titled “From the terminal”Typed with nothing after it, mnema prints a short summary on stdout and exits 0 (at a terminal it asks
what you want to do instead): what the product does in a sentence, whether a project is founded here and how
many decisions await a judgement, which channels are off on this machine and the command that turns one on,
three verbs to go on with, and a pointer to mnema --help, which prints every command. The summary reads the
record and writes nothing; a part it cannot read (a line of the record, or the switches) is not guessed: one line says so and names mnema verify, and neither claims the project is not founded nor lists what could not be read. mnema init names the channels that begin off in
two lines before its “Next” line, and mnema switch ends each off line with the command that turns it on
(every switch).
# A … marks where this page shortened what a command prints: a path, a 64-hex id, the# rest of a line, or, alone on a line, lines it left out.
# Found a project. This creates the record and this machine's identity.mnema init#> Initialized mnema project at /path/to/repo/.mnema#> identity: mnid:c0fc3c713f09a43384ac08f7d91fca43…#> backup key: created and enrolled, private half at …/identity/backup/….key#> Move that file off this machine: a backup left on this disk is lost with it.#> …
# `init` also leaves `.mnema/README.md`, a page for whoever opens the folder: the record changes with the# work and each change goes in the same commit as the work it describes, nothing in it is edited or# reformatted, and how to verify it. Neither reader counts it as part of the proof. In a repository that# has a `.prettierignore` or declares prettier (a dependency, a `prettier` key in `package.json`, or a# `.prettierrc*` in the project's directory or any directory above it up to the git root), `init` also adds# the line `.mnema/` to the `.prettierignore` of the nearest of those directories that declares prettier (a# subproject's own, else the root's; creating it only when prettier is declared), and says so. It also leaves# `.mnema/.editorconfig`, which sets every property an outer `.editorconfig` could impose on the record (a final# newline, an end of line, a trim, a charset, an indent) to `unset`; it has no `root = true`, so it takes nothing# else away from the folders above. And it writes the mnema instructions into the files agent hosts read at# the start of a session, naming each (see "The mnema instructions in the files a host reads", below), and# at a terminal it first asks which of four of them (Enter for all four, Ctrl-C to found nothing); with no# terminal, or with `--all-hosts`, it asks nothing and writes all four. It# touches no other file outside `.mnema/`. Run again in a project that is already founded, it adds only what that project lacks of# these (the README, the `.editorconfig`, the `.prettierignore` line), says what it added, and# touches nothing else of the record (and gives the record's own `.gitignore` a line it lacks, such as `/site/`, and says so);# where nothing is missing it answers "Already a mnema project" as before.# It never writes the instructions again: `mnema instructions` does, where they are missing.## A project founded before `init` left the README gets it without running `init` again: the first signed# write to its public record (any verb, or any MCP tool) leaves `.mnema/README.md` and `.mnema/.editorconfig`,# each missing one with a line saying so (on stderr for a verb). A read, a refused write and a write to a tree# that does not travel leave the folder as it was, and a file that is already there is never replaced. Only# these two are made this way; the `.prettierignore` is never edited outside `init`.
ME=mnid:c0fc3c713f09a43384ac08f7d91fca43… # the identity printed above
# Create a task. It is named by its id; `t-0b76` is a display alias derived# from that id, not a second key you can look it up by.mnema task create "Ship the parser"#> Created task t-0b76 (0198f3c1-7a2e-7b41-9c05-3d8e6f2a1b44)#> …
TASK=0198f3c1-7a2e-7b41-9c05-3d8e6f2a1b44
# Move it through the workflow. A task opens in DRAFT, so it is submitted# before it can start; the gate decides which moves exist, not the command.mnema task move submit "$TASK"#> Task t-0b76 (0198f3c1-7a2e-7b41-9c05-3d8e6f2a1b44) → READYmnema task move start "$TASK"#> Task t-0b76 (0198f3c1-7a2e-7b41-9c05-3d8e6f2a1b44) → IN_PROGRESS
# Some moves require their evidence, and the gate refuses without it.mnema task move complete "$TASK" --note "parser ships"#> Task t-0b76 (0198f3c1-7a2e-7b41-9c05-3d8e6f2a1b44) → DONE
# Ask what the workflow allows next.mnema task next "$TASK"#> Task 0198f3c1-…: 1 legal move(s):#> reopen → IN_PROGRESS (needs reason)
# Or dry-run a move without writing anything. The actor is an anchor id: the# identity `mnema init` printed above.mnema task guard reopen "$TASK" --actor "$ME"#> REFUSED (MISSING_PROOF): "reopen" requires a non-empty "reason"
# Verify the record: hash links, signatures, and what is not yet covered. One# verdict per tree of the project, under the tree's name.mnema verify#> public: local integrity verified (T1/T2/T4); 1 tail(s); all events are signature-covered; …# `mnema init` declares the backup in the record, so every clone and CI runner says that# key the same way; a `census [key-without-tail]` is a tail to look for. A clean record is# that one line: `--verbose` adds the census note about the backup key and, when the private# tree holds nothing, the line that says so. It adds and never takes away: a break, an# issue and the exit are the same with it and without it. It also adds, per agent name a# fact carries, how many facts name it on the word of the key that signed (`declared`); facts# an agent key signed are counted without the flag (`attested`, FORMAT.md §6.2).
# Auditing several projects? Name them, and get ONE verdict over all of them.mnema verify --workspace ~/work/api ~/work/web#> /home/you/work/api public: local integrity verified (T1/T2/T4); …#> /home/you/work/web public: local integrity FAILED: see issues; …#> …#> 2 path(s) named → 2 distinct: 2 project(s) covered. …A task can carry what it is, and a handoff what the next agent must read. mnema task create "<title>" --description "<text>" and mnema task handoff <task> <from> <to> --brief "<text>" (the create_task and
record_handoff tools take description and brief) record up to 64 KiB of text in the same call as an
observation about the task, under the topics description and brief, and the reply names the observation’s id.
The record’s format does not change: the text is a note, signed as a note is, and not a field of the task.
mnema show <task> and read_record then serve the description, the last handoff (from, to and its brief) and the
notes about the task, newest first, cut at eight with the count the cut left out. mnema status, bootstrap and
next_actions say of each task in flight who has it (the to of its last handoff, or whoever started it) and the last
thing that happened to it, on one line; next_actions says it in two lines of its own after the list of moves. A
handoff that names no --scope follows its task to the tree that holds it, brief included. show and
read_record count the notes about a task over every tree this machine holds. mnema refs <path> and audit_refs,
for an id no tree authored that reads as a path under the project, add whether the file exists now and the SHA-256
of its bytes: a reading of the working tree, written nowhere. A path that leads out of the project, even through a
directory link inside it, is not read; a link that stays inside is followed. mnema search "ADR-48" finds the decision by its label, as mnema show ADR-48 does.
mnema verify exits non-zero when the record is broken (in either tree, in
any project it was told to cover, and the line says which), so it drops into
CI as a check with no extra wiring. It also exits non-zero when a named path
holds no record at all, because a set that quietly skipped a mistyped path would
be a green gate over a project nobody verified; --allow-no-record accepts the
gap and rules on the rest, and it never accepts a break. A path whose .mnema/ is there
but holds no tail with anything in it is a project, not a gap: it is ruled on, and refused
(below), with the flag or without it.
mnema verify --json prints the reading as one JSON document, and two of its fields answer the
question a script asks. ok is the verdict on the record, the same as record.ok: true when
every tree it covered verified, false over a broken record (it used to be true there, because it
said only that the command ran) and false where nothing was covered (a set in which no path holds a
record has no record field). requirementMet says whether the level --require asked for was
reached. The exit code is the one the prose reading has, and --json changes the shape of the
answer and never the answer. What no --require value answers is in notAnswered.
A break is not the only way a record can lie, and the default does not catch the
others. This paragraph used to say verify exits non-zero “when the record is
broken” and leave it there, which reads as a promise about forgery. It is not one.
Measured, against a record forged by hand: rewrite one fact, recompute the hash
links with this product’s own function, empty that tail’s checkpoints.jsonl, and
mnema verify exits 0, printing local integrity verified (T1 only): no signature was checked, while mnema search serves the forged fact. Nothing is
broken there: the hash chain is consistent, and the signatures that would have
contradicted it are gone. A crude edit (a sed, no rehash) is caught and exits
1; what walks past the default is somebody who can rebuild the chain.
What catches that record is --require=signed, which exits 1 on it, and the two
exit codes are asserted side by side in
packages/code/tests/the-strict-gate-catches-the-forgery.test.ts. So:
What “broken” means is the caller’s to declare. --require=signed also fails
when any event of any tree it covered is not covered by a verified signature: every
write here signs what it wrote, so it passes whenever nothing is mid-write, and what it
catches is checkpoints that did not verify or were taken out from under the events
they signed. A cut that took the newest events WITH their checkpoint is not caught:
what is left is a shorter record honest in every byte. --require=witnessed also
fails when no external witness dates the record (mnema witness stamp asks for one;
it passes once a Bitcoin block carries it, and never while it is pending). Asking
costs nothing: --require is a comparison of levels, not extra work.
The default stays --require=chained (a break and nothing else) and the reason
is not the one written here before. That read because events above the last
checkpoint are the normal state of a session in flight; every write seals a
checkpoint now, so --require=signed passes in every state this product reaches and
the gate that “always fails” fails in none of them. The reason that survives is about
what a stranger is told. There are three ways to forge a record and the strict gate
catches one: the attacker who edits a tail’s events, drops that tail’s checkpoints,
and neither removes the whole tail nor writes a fact afterwards. Made the default, it
would hand somebody who cloned a record with a whole person’s history deleted
verified (T1/T2/T4); all events are signature-covered, exit 0, plus the belief
that a strict gate had ruled on it. The permissive default at least says no signature was checked over the record it cannot vouch for.
And what neither value answers, said plainly. One tail removed whole while another
remains is reported but is not a break: verify crosses the committed keys against the tails on disk and prints
N committed key(s) without a tail (see census: informational, not a break), exit 0.
A key file whose name is not the fingerprint of the key it holds is not that note: it is refused,
whether or not anything signs with it (FORMAT.md section 6). Nor is a .pub under keys/ that holds
no key (a directory, bytes that are not UTF-8, or no PEM-wrapped Ed25519 key): it is refused by name, with
the reason, in the same words by verify, the page and the Python verifier.
One key is said otherwise: the backup mnema init creates never signs until it is
restored, and init declares it in the record (backup.declared), so every machine reads
N backup key(s), which sign nothing until restored, and its census line (--verbose) still says that a backup
restored and used would have left a tail that is not there. A record written before that
declaration existed does not say which key is a backup: there the key reads as a backup only
on the machine that made it, and as a committed key without a tail anywhere else.
A tail that holds nothing (its directory with only its ownership proof, which an older
version left when a key’s first write was refused) is not counted as a tail: verify says
N empty tail(s), which hold no event and are not counted, and it moves neither the level nor
the witness. A tail emptied of its events with its proof kept reads the same way.
A tail removed together with its key, while another remains, may leave nothing to report at
all, and only a history outside this record (the one a git remote keeps) can testify to what
was taken out.
A record in which no tail holds anything is refused. No tails/ directory, no tail
directory in it, or only tails that hold nothing: such a record asserts no fact, and verify
fails it by name at every --require, exit 1: the one-line verdict says local integrity FAILED, and the issue reads issue [T1] public the record: NO TAIL: no tail in this record holds an event or a checkpoint: there are no tails, …, with the reason as it found it. The
census still names each committed key with no tail, so a record whose one tail was deleted
says whose. A record is never in that shape by being founded: init writes the founding and a
checkpoint before it returns; it is in it when every tail was removed, by a merge, a sparse
checkout or a commit (whether or not the keys went with them), and when .mnema/ was left
half made: an init that failed after creating the directory leaves .gitignore,
.gitattributes and at most the README.md and nothing else, and that project fails verify until a first write to the
public tree founds it (a second init answers that there is nothing to found, and does not).
FORMAT.md §4 is the rule, and the second reader refuses the
same records under it.
What closes it in CI is the history git already keeps. Four records exit 0 even
under --require=signed: the newest events cut together with their checkpoint, the
record erased and founded again, a key holder rewriting the middle and signing it
again, and one tail removed whole while another remains. Each
leaves a record honest in every byte, and each changes files that the commit the change
started from already held. So --since <rev> asks git: every file of the committed
record at <rev> must still be on disk, and every segment and checkpoints.jsonl must
still begin with exactly the bytes it held there: a record only ever grows at the end. A
tail removed in a commit older than the revision --since compares with is in neither, so
--since does not find it; a record with every tail removed that way still fails the default verify. It
reads git and never the network, and changes nothing when it is not asked for. In CI,
BASE is the commit the change is measured against: the target branch of a pull
request, fetched (a shallow checkout holds no base, and --since says so and fails):
mnema verify --require=signed --since "$BASE"It fails on a cut you authorized with mnema tail prune too, and that is the point of it:
the change that removes a tail is the one change a reviewer has to look at. It rules on
the committed tree only (the private one never travels), and it trusts the base: a
history rewritten on the remote moves the base with it, which is what a protected branch
on the git host is for.
Who signed, where somebody asked. A record founded again under a fresh key verifies
like an honest one, because nothing in it says whose key is whose. mnema key github <name> records a signed claim that this identity is that GitHub account, and mnema verify --against-github then compares the key each identity signed with against the SSH
keys the account publishes at https://github.com/<name>.keys, by the raw Ed25519 key.
Per identity it says the account publishes every key it signed with, or says by name
why not: no account linked, no such account, no Ed25519 key published, a key that is
not among them, or github.com could not be reached. What it proves is that the key
that signed is one that account publishes today. What it does not prove is that
the key was the account’s when it signed, since an account adds and drops keys at will,
and it proves nothing without taking github.com’s word for which keys an account has.
Only a signed claim counts: one in the window above the last checkpoint could have been
appended with no key at all. It goes to the network only when the flag is given; the
verdict, the level and the exit are the ones verify gives without it, and with no
network the verdict still stands and each identity reads could not reach github.com.
A Sigstore countersignature, where somebody asked. mnema witness sigstore signs the
last checkpoint of each tail with a short-lived Sigstore
certificate, through the browser here, or with the job’s own token in GitHub Actions
(permissions: id-token: write), and files the bundle beside the .ots. Of the record,
only the checkpoint’s digest leaves. Your identity leaves too, by design: the
certificate names the e-mail you sign in with, or the repository, the workflow, the commit and the
run, and it goes into Sigstore’s public log, which does not forget, and into the committed bundle; in
GitHub Actions the act refuses a private repository. mnema key sigstore <e-mail or workflow> records the signed claim that this identity is that one (an e-mail as its
SHA-256, never the address, which protects the record and not the bundle or the log) and mnema verify --against-sigstore reads every bundle offline, against the trust root the binary
carries, and says who signed in, when Rekor logged it and, from a GitHub Actions job, the
repository, the workflow, the commit and the run the certificate names, and whether an
identity of the record names that signer. What it proves is that whoever could sign in as that e-mail (or run
that workflow) countersigned the checkpoint by then, on Rekor’s clock. What it does not
prove is who wrote the record: anybody can countersign the digest of a checkpoint they can
read. It is no witness level: the verdict, the level, --require witnessed and the exit are
the ones verify gives without it.
Bold, dim, and what a pipe gets
Section titled “Bold, dim, and what a pipe gets”In a terminal, a verdict’s label and a heading are bold, the half of a statement after the colon is dimmed, and an id or a date in a list is dimmed so the title beside it is what the eye lands on. In a pipe, a file or a CI log, nothing is: the capability is resolved once per invocation and the default is what the destination can show.
mnema --color=never verify # never any escape, whatever the terminal saysmnema --color=always verify | less -RNO_COLOR=1 mnema verify # and FORCE_COLOR, both as everywhere elseAn explicit --color beats both variables. The help says only the three values and the two
variables, to keep its first page short; this is the rest of it.
Style never changes what a line says. Strip the escapes and the styled output is the plain output, byte for byte: the same text a redirected file holds, so a verdict quoted from a terminal is the verdict.
Colour says three things and no more, and each hue means one thing whichever answer
sets it. Red means the command did not do what you asked: a refusal from the
workflow, a value the surface turns down, a command line the parser turns down: all
three are the same news, and all three are red. ALLOWED is green for the same reason.
And verify paints how far the proof got: green when every event is covered by a
signature that was checked, yellow when the hash chain held and no signature was checked
at all, red on a break. Only that one clause of the verdict is coloured (the tree’s name
beside it is not, because the tree is not the news) and the words are the chain’s own,
unchanged.
The third is a record’s position in its workflow, which a list and mnema show print
in parentheses after the title. A task’s (BLOCKED) is red, (IN_REVIEW) is yellow and
(DONE) is green; a decision’s (proposed) is yellow and (accepted) is green; a
pattern’s (proposed) and (reviewed) are yellow and (adopted) is green. The other
eight states carry no colour at all. That reads like a taste and it is read off each
machine’s transition table: BLOCKED is the one state whose only move UNDOES it, so it
is the one position that cannot progress; a position left only by moves that demand a
verdict from somebody is yellow, which is IN_REVIEW, a proposed decision and a
pattern awaiting review or a ruling; a position that ARRIVED and can only be undone at
the cost of an explanation is green, which is DONE, an accepted decision and an adopted
pattern. Where a state is the ordinary course of work, or is over (cancelled, rejected,
superseded, retired), there is nothing for a reader to do about it and nothing is painted,
which is most lines.
Green on (accepted) is the part that is easy to get wrong, so it is worth saying why it
is not yellow: supersede stays legal from accepted for good, so anything built on
“this still has a legal move” would report every call the project ever settled as a
pendency that never clears. That is not hypothetical: the opening read’s work list was
built on exactly that rule, and it listed every completed task as work to pick up until
it started asking what the position MEANS instead.
This section used to say colour meant two things, and that a state would never be one of
them because states are categories and a hue per category is noise. The rule held and
the classification did not: a state is a position in a cycle whose exits differ, which is
why sixteen states collapse into three hues instead of sixteen. The rule that survived is
the one about hues: no tree is told from another by colour, and the scope column carries
none. What it does carry is the DIMMING an id and a date carry: a column that answers the
same word on every row of a reading is a column a reader has to look past, and taking the
weight off it is what makes the title beside it the subject of the line. It also used to say the colour was a task’s alone, which was
true of the code and never of the rule. Asserted end to end in
a-state-is-a-position.test.ts, and the derivations from the tables of moves in core’s
disposition.test.ts and context’s.
A count is still not coloured: three reopened tasks may be a team learning something, and this tool does not decide that for you.
The hue never replaces the word that says it, which is why --color=never, a pipe and a
monochrome terminal lose nothing.
Finishing a verb with Tab
Section titled “Finishing a verb with Tab”mnema completion <shell> prints a completion script for bash, zsh or fish. It installs
nothing and writes to no file: where the script goes is your choice:
source <(mnema completion bash) # this shell, nowmnema completion bash > /etc/bash_completion.d/mnema # every bashmnema completion zsh > "${fpath[1]}/_mnema" # every zshmnema completion fish > ~/.config/fish/completions/mnema.fish # every fishIt completes verbs, subcommands and option names (including an option a parent group
declares where the subcommand reads it: mnema task move --which; one it does not read is
refused there, and not offered) and a value only where the declaration enumerates one. That last one now covers every closed set the
domain owns: the ten workflow actions of task move and task guard, the two of decision move, the four of skill move, the three scopes of every --scope (two on decision import, which leaves out the machine-global tree), the levels of
verify --require, the kinds of search --kind, the directions of refs --direction.
It does not complete an id, and that is a decision rather than a gap: an id is in
no declaration, so answering would mean running mnema on every keystroke, and a run of
it costs about 95 ms: more than the command being typed. This page used to say the same
of a transition, on the same argument. What falsified it: a transition IS in a
declaration now (the sets moved into vocabulary.ts, where a declaration names
one without commander validating it), so offering the ten actions costs a Tab nothing and
the workflow gate still owns the vocabulary (mnema task move nonsense <id> is refused
UNKNOWN_ACTION, by the gate, exactly as before). Asserted in
one-source-for-a-vocabulary.test.ts, on the declarations and on the real binary.
The script is generated from the program’s own declarations, so a verb added to mnema
is completed the day it ships; print the script again after an upgrade. Asserted in
the-shell-knows-the-verbs.test.ts, which enumerates every command and option from the
program rather than from a list, and drives the generated bash in a real bash.
When an agent is the one running the CLI
Section titled “When an agent is the one running the CLI”Omitting the agent says a person acted, and that is what the record then
asserts. So an agent driving the CLI (a script, a CI step, an agent with no MCP
server) names itself with --which, on every verb that writes:
mnema task create "Regenerate the fixtures" --which release-botmnema task move complete "$TASK" --note "fixtures regenerated" --which release-botmnema audit accountability then separates its work from yours instead of crediting
both to you. It does not change where the work lands: a decision an agent records is
the project’s decision, the same as yours. (mnema memory and mnema observe are
the exception: for those two, declaring an agent still sends the capture to this
machine’s private tree, because the kind cannot say who a note is for.) Naming your
own identity as the agent is refused: whoever authorized the work cannot also be who
executed it.
Nothing verifies the name: --which is a declaration, exactly as the MCP server’s
is the client’s own announced name, unless an agent key signed it (then the name the record enrolled the key under). What is proven is the signature: the
identity that authorized the write, which is derived from the key and never typed.
The one value refused is a --which that names nobody: --which "$AGENT_NAME"
in a CI step where the variable never got set. Omitting the flag is how you say a
person acted; passing it empty would say the same thing while looking like it said
the opposite, and credit you for the agent’s work.
Framing that work in a session
Section titled “Framing that work in a session”--which names the agent on each fact. A run frames the session those facts
belong to, and records that you opened it for that agent, so the work is not
just attributed to an agent, it is authorized by a person. The MCP server opens
one per connection; on the command line you open it yourself:
mnema run start --which release-bot --goal "regenerate the fixtures"#> Started run 019fa572-32c2-7780-b1a7-0fe895a1c7ef#> for release-bot: regenerate the fixtures#>#> export MNEMA_RUN=019fa572-32c2-7780-b1a7-0fe895a1c7ef
export MNEMA_RUN=019fa572-32c2-7780-b1a7-0fe895a1c7ef
# Every fact written from here on names that session.mnema task create "Regenerate the fixtures" --which release-bot
mnema run end --which release-bot --outcome "fixtures regenerated"unset MNEMA_RUNWhile it is open, mnema resume --actor "$ME" lists it among the runs still open;
afterwards the same command shows where it left off. Two rules keep the record
honest: a run always names an agent (one that named nobody would prove no
delegation), and the id in MNEMA_RUN is checked against the record before
anything is written: a run this project has no record of, or one already ended,
refuses the write instead of stamping a session nothing can vouch for.
An MCP session ends its run when the connection ends: the client hanging up, or the
host asking the process to stop. A process killed outright records nothing on the way
out, so its run stays open, and nothing here closes a run it did not open: no rule can
tell an abandoned session from a live one that is idle, and two sessions running at
once make every such rule wrong. So resume reports what can be known (how long each
open run has been open, and how long since anything was recorded in it) and
mnema run end <id> --which <agent> is how you close one yourself.
Working the CLI yourself needs no run: the identity that signs each fact already carries the authority a run exists to delegate.
What those sessions cost, which the record does not hold
Section titled “What those sessions cost, which the record does not hold”mnema usage puts a run beside what Claude Code spent while it was open:
mnema usage#> 2 run(s):#> 019fa572-32c2-… release-bot 2026-08-13T09:02:11.004Z → 2026-08-13T11:40:52.881Z in 4211 · out 88190 · cache-read 3120044 · cache-write 502113 tokens · 214 message(s) · claude-opus-5 · session 03e234a3-89fc-…#> 019fa4e1-77aa-… release-bot 2026-08-12T18:18:34.870Z → open no transcript#>#> Read from /home/you/.claude/projects: 7 host session(s) there record work in this project.#> These counts come from Claude Code's own transcripts on this machine, not from the record: …Nothing is recorded, and there is no field for it. The chain deliberately has
none: the host’s transcripts expire, so a cost written into the record would be a
signed number whose only witness deletes itself. The verb reads those transcripts for
usage, the model id and a timestamp (never a line of the conversation) and writes
nothing anywhere.
It refuses to guess. One host session inside a run’s window is attributed and
named, so you can open the same file it read. More than one is named and not
attributed: there is no tie-break that is not invented. None says no transcript,
never 0, because “nobody recorded what this cost” and “this cost nothing” are
different pieces of news.
Tokens and a model id. Never dollars. No price table is carried, so nothing here can quietly age into a wrong figure.
Which host session belongs to which run is the command’s own inference from the two clocks: the record does not state it, and the last line of the output says so.
How often each tool was called, which the record does not hold either
Section titled “How often each tool was called, which the record does not hold either”mnema usage --tools counts, per tool of the MCP server, how many times it was called, how many of those calls
were refused, and the first and last date:
mnema usage --tools#> 3 tool(s) called:#> search only reads 3 call(s) 0 refused first 2026-10-20T22:57:06.349Z, last 2026-10-20T22:57:06.351Z#> capture_memory writes 2 call(s) 0 refused first 2026-10-20T22:57:06.337Z, last 2026-10-20T22:57:06.348Z#> read_record only reads 1 call(s) 1 refused first 2026-10-20T22:57:06.352Z, last 2026-10-20T22:57:06.352Z#>#> Read from /home/you/.mnema/tool-calls/01fe2ff055134f23.jsonl.#> This counts only the calls the MCP server on this machine answered for this project: …It is a file on this machine, and nothing else. For each call the server answers and counts it appends one line to
<data directory>/tool-calls/<digest of the project>.jsonl (~/.mnema, or $MNEMA_HOME): the instant, the
tool’s name, whether the tool writes (mutates) or only reads, and ok or the code the refusal carried. Never an
argument, never a word an agent wrote, never a record’s id. It is not an event, it is not signed, mnema verify does
not read it, it is in no tree of the record and never in the repository, and no format changed.
It counts only what this server on this machine answered. A call the server refuses while validating its arguments (they do not match the tool’s input schema) is not counted: it never reaches the tool. A call another host or another machine answered is not in it, nor is any use of mnema outside the MCP, nor a call made before the machine had a key of its own (the count makes no directory of its own on a machine where nothing has signed yet). The output says so on its last lines.
It is on until you switch it off, because what it keeps is local and holds names and nothing else:
mnema switch off tool-call-count stops the count (a fact of the record like the other switches), and mnema usage --tools
then says that the count is off and that what it shows was kept before.
The session journal
Section titled “The session journal”At each Stop mnema tally keeps a small file for the session, at
<data directory>/sessions/<digest of the project>/<the host's session id>.json (~/.mnema, or $MNEMA_HOME), when the hook
names the session (Claude Code does, as session_id). It lets the next Stop read the transcript from the byte where the last
reading stopped instead of the whole conversation again, and it holds, in ids and counts, what the session did with the
record: the decisions recorded since it opened (their ids and state), the notes of the host’s memory copied into the private
tree (their ids), how many questions the person answered with no decision recorded after, how many files that hold decisions
were written and are cited nowhere in the record, how many files were edited, the tasks in progress and the runs open, the
goal of a run (the title of the first task it moved into progress in the tree that run is written to, a title that tree
already holds) and an outcome that is a sentence of counts of that tree’s records. The count of questions answered with no decision recorded after is, in the private run, over the decisions of both trees after the question, and in the public run over the public tree’s decisions only: it crosses from the public tree to the private one, never the other way. status prints the last run of each tree that has one, the tree named, where a session left runs in more than one. Never a prompt, the text of a message, a tool’s output, the content or diff of a file, the
environment, a secret or the answer a person chose: a file is kept by a digest of its path, and the one path kept is the
name of a file that holds decisions. The bytes of the transcript already read are kept by a digest of their first and of their
last 4096, so a transcript that shrank, or whose head or tail changed, is read again from the top; a rewrite that leaves the
size and both ends alone is not told, which is what keeps a Stop from reading the whole file. The file is mode 0600, and its directories
0700, whatever the umask.
It is disposable, and outside the record. It is not an event, it is not signed, mnema verify does not read it, it is
never in the repository, and deleting it loses nothing the record holds: the next Stop reads the transcript from the top
and writes it again. The journals of a project are pruned at each write: those untouched for 30 days go, and all but the
newest 50.
What reads it. The MCP server, when the host leaves its session id in the server’s environment (Claude Code’s
CLAUDE_CODE_SESSION_ID): the run the server opens for the connection takes the journal’s goal as its own goal when one is
known by then, and the run it closes ends with the journal’s outcome (for example 2 decisions recorded; 5 files edited, as of the session's last Stop.: it is what the journal held at the last Stop, so a decision recorded after that is not in it).
Each comes from the records of the tree the run is written to and from no other: the title of a private task never becomes
the goal of a run in the public record, and a public run’s outcome counts no private decision or note.
The resume and status readings print that outcome beside a run that ended with one, as outcome: ….
When the connection ends, the server marks the journal as ended (endedAt), and the next Stop of that session, if it goes on,
takes the mark away: it is what lets mnema recall tell a session that stopped from one that is still going.
It is on until you switch it off, because what it keeps is local and holds ids and counts: mnema switch off session-journal stops it (a fact of the record like the other switches); with it off, every Stop reads the whole
transcript, as before.
After a compaction
Section titled “After a compaction”A compaction replaces the conversation with a summary, and the decisions a session recorded are among the first things a summary
drops. The brief and the notes come back on their own (a SessionStart fires after a compaction too) and say what the project
holds; what is lost is what this session had done with the record. So the plugin has one more SessionStart hook, under the
compact matcher, that reads the session’s id off the payload and runs mnema recall --after-a-compaction --session <id>.
It prints one sentence read from the session’s journal, about sixty to a hundred and twenty tokens, and nothing else. The decisions in it are
the record’s, not the journal’s: those created since the session opened, in the state they have now, because the journal is written at the end
of a response and one recorded or moved since is not in it. That is every decision that entered the record since the session opened,
so one that came by a pull, or that another session recorded, is counted too: the sentence says “entered the record”, not “recorded by
this session”. It names five at most (then and N more), and a label that two trees both number (each tree numbers its own) says
which tree it is of, as ADR-1 (private):
Before this conversation was compacted, this session had: 2 decisions entered this project’s record since this session opened (ADR-12: proposed; ADR-13: accepted); 1 note of this host’s memory copied into this machine’s private tree; 1 question answered with no decision recorded after; its own tool calls wrote 5 files.Each part is left out when it is zero, and where nothing is left it prints nothing. It is a count, with the labels of decisions the
record numbered, and none of the session’s words (not even the goal): a fact and not an order. It records nothing. It is silent for
a session with no journal, for a payload that names no session, and for a mnema older than the flag (which refuses it and is
not asked again without it). mnema switch off after-a-compaction stops it, and so does switching the journal off.
This is Claude Code’s hook; the other hosts’ hooks files do not carry it, since none of them was measured to run a hook after
a compaction.
What a session handed to other agents
Section titled “What a session handed to other agents”When a session dispatches work to a subagent (Claude Code’s Agent tool), a PostToolUse hook runs mnema dispatched. The
first dispatch of the session records a private task, Work handed over in this session and the instant (UTC, to the
minute), moved to in progress; every dispatch records a private handoff on it, from orchestrator to the subagent type
the call named (subagent when it named none), with a brief that is the call’s short description (cut at 200 characters)
and, when the prompt cites a file that exists on disk (a path ending in .md, .txt, .json, .yml or .yaml), the line
Brief in the file: <path>. The prompt is never recorded, and neither is the subagent’s reply. When the session ends
(SessionEnd), the task is moved to done with the count of handoffs as its note.
Nothing goes to the tree a clone gets: the task and its handoffs are in this machine’s private tree. The task’s id and the count
are kept in the session journal, the one file this machine keeps per session, so the mirror does nothing
where session-journal is off. A session that is resumed (the host hands the same session id) reopens its task instead of
opening a second, and closes it again at the next end. A session that ended with no SessionEnd (a terminal that was killed)
leaves its task in progress: the next session of the project that dispatches blocks (never completes) the tasks whose journal
has been untouched for a day, with a reason that says the session stopped with no end, when it was last seen and how many
handoffs it recorded, since nobody saw how it ended; if that session comes back, its next dispatch unblocks the task, and its
end completes it. A failure of the mirror never holds a dispatch: the hook exits 0, and what it could not do is a line on the
second stream. Two dispatches of one session are two processes, so each takes a lock on the journal; one that does not get it
in three seconds records nothing (no task, no handoff), says so on the second stream and counts the skip, and the task’s
closing note says how many dispatches were not mirrored. mnema switch lists agent-mirror as doing nothing while
session-journal is off.
It is on until you switch it off (mnema switch off agent-mirror, a fact of the record like the other switches), because
what it writes stays in the private tree and holds a few words the model itself wrote to label the dispatch. The cost is one
mnema process per dispatch and per session end, and no tokens: measured, on a machine at a load of 9 on 16 cores, at 340 to 430 ms
for each dispatch after the first, 860 ms for the first (it opens the task) and 530 ms for the session’s end, of which 220 to 260 ms
is the start of the process (mnema --version); the numbers move with the load. The lock a dispatch takes on the journal is taken
from a process that died holding it (older than fifteen seconds) by one process only, and the Stop that keeps the journal
waits for it too.
Bringing a second machine into your identity
Section titled “Bringing a second machine into your identity”Your identity is one anchor with several keys, and each machine holds its own key: the private half never travels. So a laptop joining your desktop’s identity is a handshake in three steps, run on the machine each step belongs to:
# On the NEW machine, in a clone of the repo (or anywhere: this touches no# record). It creates this machine's key if it has none, and prints a request.mnema key request --anchor mnid:c0fc3c713f09a43384ac08f7d91fca43…#> Created this machine's key 8f21ab…#> to join mnid:c0fc3c713f09a43384ac08f7d91fca43…#>#> mnema-key-request:1:eyJwdWIiOiItLS0tLUJFR0lO…
# On a machine ALREADY in that identity, inside the project. Paste the line.mnema key enroll 'mnema-key-request:1:eyJwdWIiOiItLS0tLUJFR0lO…'#> Enrolled key 8f21ab…#> into mnid:c0fc3c713f09a43384ac08f7d91fca43…#> Commit and share the record: the other machine joins by reading it.
# Commit and push, so the new machine can read the vouch. Then, back on it:mnema memory "first note from the laptop"#> Captured memory 019fa4ef-1425-72f9-9058-0cd55236376fThat last write is where the new machine settles which identity it serves here: it reads the vouch out of the record and speaks as that anchor from its first fact onward. Nothing was decided by asking: only by being vouched for.
The request is not a secret: it is a public key plus a signature over public values, so it is safe to paste into a chat or a ticket. It proves consent to join one identity and is worthless to anyone who is not already a member of it: only a member’s vouch turns it into a fact. And a machine never admits itself: that is why the middle step runs somewhere else.
mnema audit accountability is the check that it worked: one author, not two.
To take a key back out (a stolen laptop, a leaked backup copy):
mnema key revoke 8f21ab… --reason "laptop stolen"Retirement is forward-only: what that key signed while it was a member stays valid, because a rotation should not make past work unattributable. The identity’s last key cannot be retired (it would leave an identity unable to sign anything again, including its own repair), so bring the replacement in first.
Where your private key lives, and a passphrase for it
Section titled “Where your private key lives, and a passphrase for it”Each machine signs with one Ed25519 key, and its private half is one file:
<MNEMA_HOME>/identity/keys/<fingerprint>.key (~/.mnema/identity/keys/… when
MNEMA_HOME is not set). That file is the key’s PKCS#8 PEM in the clear, created with mode
0600 and never re-checked afterwards, in a directory made with your umask. The cold backup
mnema init makes sits beside it, in identity/backup/, in the same form. Whoever can read
those files can sign as your identity, and a copy of the disk (a stolen laptop, a synced home
directory, a backup) is a copy of the identity. Nothing in the record protects the key; the
proof is only that a signature was made by it.
You can put a passphrase on it, and nothing else changes. At a terminal mnema key protect asks
for it, twice and with nothing echoed; the variable does the same where there is no terminal:
mnema key protect # asks twice#> New passphrase for the private key:#> Again:#> Protected 2 of 2 private key file(s)#> …export MNEMA_KEY_PASSPHRASE='a long passphrase of your own' # or, with no terminal to ask atmnema key protectprotect encrypts the key and its backup in place (scrypt, then AES-256-GCM, written
atomically with mode 0600). The signed format is untouched: it is the same key and the same
signatures, no event or checkpoint says whether a key is protected, and mnema verify never
opens a private key, so a record written with a protected key verifies with no passphrase at all.
Anything that signs needs the same passphrase: at a terminal, mnema asks for it once per
command; where there is no terminal (a script, a pipe), it needs MNEMA_KEY_PASSPHRASE set to the
same value. An agent’s host and a hook have no terminal either, and are not the place for the
passphrase: they sign with a key of their own (key request --agent, key enroll --agent, below). With the passphrase missing or wrong a write is refused
(KEY_IS_PROTECTED, KEY_PASSPHRASE_WRONG), nothing is recorded, and no second identity is
created on the way. mnema key unprotect writes the files back in the clear, and a wrong
passphrase changes none of them.
What it does not do: a process running as you can read the variable from its own environment and the opened key from the process that holds it; a keylogger sees the passphrase typed; and a short passphrase is a short passphrase. It protects the key at rest, which is what the file in the clear did not. The prompt appears only where both the input and the error stream are a terminal, so a server, a hook or a pipe is never made to wait for a line nobody will type; the system keychain is not a source. A lost passphrase is a lost key, the cold backup included if it was protected too: keep the passphrase where you keep that copy.
Authorizing a cut, which is not the same as making one
Section titled “Authorizing a cut, which is not the same as making one”Deleting a machine’s tail from a record is invisible to the proof: verify reports
the same sentence a machine that never wrote anything gets. So a cut is declared
before it happens, while the tail is still on disk and everything the declaration
claims can be checked against it.
The declaration names a tail, and tail list is what spells one: with what it holds,
the tree the waiver will land in, and whether a cut of it is already authorized:
mnema tail list#> 2 tail(s) holding events (the ones a cut can name):#> 8f21ab…-3c9d0e… public 41 event(s) through 5e4391d8… the tail of mnid:60a50d38… · no waiver#> 4b90cd…-7ae112… private 12 event(s) through 9c02f7a1… the tail of mnid:c0fc3c71… · no waivermnema tail prune 8f21ab…-3c9d0e… --reason "the person asked to be taken out"#> Authorized the cut of tail 8f21ab…-3c9d0e…#> 41 event(s) through 5e4391d8…, the tail of mnid:60a50d38…#> authorized by mnid:c0fc3c71…#> Landed in the public tree: committed with the repository, so it reaches every clone.#> The tail is still on disk at /path/to/repo/.mnema/tails/8f21ab…-3c9d0e…: nothing was removed.#> Git history, any remote and every clone still hold what that tail recorded: …It removes nothing, and that is the design. Removing the directory is your act,
afterwards; this command records who authorized it, how many events were there and
the head they ran through, so verify can later name the account instead of listing
the three reasons a key might have no tail. It writes from another tail (a
waiver inside the tail being cut would be cut with it), so the tail it names is never
the one this machine writes to.
And it does not promise forgetting. Once the record is committed and pushed, the
events are in the git history, on the remote and in every clone; reaching those takes
git filter-repo, a force push and everyone re-cloning, which is outside what any
command here does. The output says so on the line where somebody would otherwise
assume it had been handled.
There is no MCP tool for this, deliberately: a cut is the one write whose consequence is destructive, and it is authorized by a person at a shell or not at all.
A rule that carries its check
Section titled “A rule that carries its check”A decision in force can name the program that checks it, and a machine of its own can record, at a commit, whether the rule held.
# A person declares the check. The program and its arguments, one by one: there is no shell, so# nothing in them is split, globbed or expanded; an argument that starts with a dash goes after `--`# (`mnema check declare <decision-id> node -- -e "process.exit(0)"`).mnema check declare <decision-id> node scripts/check-cents.js
# On the machine that will run checks (a CI runner), once: a key of its own, for ONE identity.mnema key request --checker --anchor <identity># A member of that identity enrolls the line it printed, and commits the vouch.mnema key enroll --checker <the line>
# On the runner, at a clean commit: run the check of every rule in force and record each result.mnema check run --key <the checker's private key file>The key file has to hold an Ed25519 key, like an agent’s: any other kind is refused (UNREADABLE_KEY) before anything runs.
A key holds one role: key enroll --checker refuses a member’s key (A_MEMBER_KEY) and an agent’s, in use or retired (AN_AGENT_KEY).
check run starts each declared program from the project root with this process’s environment, minus every
MNEMA_* variable (that narrows what the program is handed, not what it can read from disk),
stops it after --timeout seconds (300 by default) and records it as failed. It writes one
check.passed or check.failed per rule, naming the rule and the commit HEAD names, with the end
of what the program printed (at most 4000 characters, control bytes made visible, credentials
redacted). It exits non-zero when any check failed. It refuses a tree that has changes outside the
record or is not at a commit, because a result names a commit. The results are written to the
working tree, where the caller commits them; nothing here pushes.
The key can say that a check passed, and nothing else. A checker key is enrolled by a member
of an identity, and a key that is already a member of one is refused. mnema verify refuses a
check.passed or check.failed signed by any other key, and refuses any other fact signed by a
checker key, so a leaked runner secret cannot decide a rule or write a note. mnema audit accountability
lists the checker as a machine, apart from the people.
A leaked key is retired. Any member of an identity can take the role away, as any member can grant it; the key is not asked:
mnema key revoke --checker <fingerprint> --reason "<why>"Commit and push the retirement, and enroll a new key for the runner. From the retirement on, mnema verify refuses a result the old key signs, any other fact it signs, and an enrolment of it again;
check run refuses to run with it. The results it signed before still verify, because the key held
the role when it signed them, and verify names them in its census
(census [retired-checker] …): a leaked key can date a result before its own
retirement, so the record no longer vouches for them. mnema audit accountability says who retired it.
What it does not promise: a result says that a key your team enrolled reported the rule held at that
commit. It does not say the program ran as it was declared on that machine, and it does not say
the program checks what its rule says. A declared program runs on the runner, so whoever can commit
a declaration can make the runner start that program. The Action in packages/action runs the
step above for you when it is handed the checker key as a secret (checker-key), and leaves the
results in the working tree for your workflow to commit or upload.
A key that signs for its person, under a name
Section titled “A key that signs for its person, under a name”The record can also say that a key is an identity’s agent: a key a person lets a program sign with,
without handing it the key that enrols, revokes and declares. A member of the identity writes an
agent.enrolled naming the key and a name, and the key signs, over the UTF-8 of
agent-enroll:<anchor>:<agentFp>:<name>, that it consents to that name. mnema verify judges those
two facts (agent.enrolled, agent.retired), and three verbs write them.
Making one, enrolling it, retiring it.
mnema key request --agent claude --anchor <identity> # where the agent's key will livemnema key enroll --agent <the line it printed> # where your own key ismnema key revoke --agent <fingerprint> --reason "<why>" # when it should sign nothing morerequest makes the key in a folder of its own, agents/<name>/ under the data directory (~/.mnema,
or $MNEMA_HOME), never beside your own key, and writes it in the clear: the host that signs with it
has no terminal to ask a passphrase at. A name begins with a lower-case letter or a digit, then holds
letters, digits, dots, dashes and underscores, up to 64 characters in all, and it is also the folder. enroll is run where your own key
is, and asks its passphrase at the terminal where the key has one. It records agent.enrolled in the
project’s committed tree and, where the key lives on this machine, in the project’s private tree and the
machine’s global one too (outside a project, the global tree alone), so what the agent writes in each is
signed by it: one line per tree says enrolled, already or skipped, and it exits 1, with what the
other trees took kept, when one tree refuses. Where the key lives here, mnema key enroll --agent <name> takes
the name for the line; a line from elsewhere (a CI runner’s key) is enrolled in the committed tree alone.
On a machine that begins every job empty, mnema key restore --agent <name> <file> puts that key into the
agent’s folder (agents/<name>/ under the data directory): it refuses, before it writes anything, a key
the committed record does not enrol as that agent under that name, one the record retired, one that is not
Ed25519 or is protected by a passphrase, and a folder that holds another key; it appends nothing to the
record and installs nothing beside your own key. The Action’s page
has the whole recipe.
A tree created after the verb ran is not enrolled: run it again, and the trees that have the agent say
already. A key retired in any of the trees is refused in all of them, and nothing is written.
It says so when your key is unencrypted, because then anything that can read your home can sign
as you and the agent key separates nothing. key revoke <fingerprint> refuses an agent key and names
the verb that retires it, and an agent key does not count as one of an identity’s keys where the last
one may not be retired. key revoke --agent retires it in every tree of this machine that enrolled it (another machine’s private and global trees keep counting it until it is retired there, and mnema verify there, once the committed tree with the retirement is pulled, refuses in the trees it verifies what the key signed after the retirement). key request --key <file>, key enroll <line> and key enroll --checker <line> refuse an agent’s key (AN_AGENT_KEY), told by its fingerprint: one this machine keeps for an agent, in use or retired, or one a record they reach enrols as an agent, wherever its file is kept. A retired key
is never enrolled again: key request --agent <name> after a retirement puts the retired key in
agents/<name>/retired/ (it is never deleted) and makes the agent a new key under the same name, which
enroll takes in the same way. The record allows more than one active key under a name, each counted by
its own fingerprint, because a person with two machines has two keys under the name a host’s manifest
fixes; enroll says when it finds another and does not refuse.
Signing with it. mnema mcp --agent-key <name> (or MNEMA_AGENT_KEY=<name>, which a mnema from
before the option ignores where an argument would stop it; the flag wins), mnema before-a-write --host <host> --agent-key <name> (read from the variable too; the flag wins) and the verbs of the command
line that write a fact an agent’s key may sign (every writing verb but init, key … and check run, which
do not read the variable), with MNEMA_AGENT_KEY=<name> in their environment, sign with that key in each tree whose record enrols it under exactly
that name (key enroll --agent writes the committed, the private and the global tree), and the which
of every fact is the name the record of the tree it lands in gives the signing key, whatever the client
announced. In a tree whose record does not enrol it (a project the verb has not been run in, a tree made
since, a machine with no key under that name), the person’s key signs, and the which stays declared;
where your own key is protected and MNEMA_KEY_PASSPHRASE is not in the environment, or is not on this
machine, the write is refused (AGENT_NOT_ENROLLED; in the hook what is not done is the recording of the fact, and the
reply to the host is unchanged and says why), naming the verb key enroll --agent, and nothing is signed, minted or founded. The
server opens and serves its reads on a machine where no key signs, each write is refused in the tree it was for, and a
session that only reads makes no key. The
server says, on its error stream, one line for each tree where the agent is not the one that signs, when it opens
and again when the session lands on a project; a name that is no agent’s at all is said once, when it opens, and not
again for each tree or when the session lands. A key the record retired in any tree the process reaches is
not passed over to yours: its writes are refused in all of them (REVOKED_KEY). A key the record proves a member
of two identities signs for neither (AMBIGUOUS_MEMBERSHIP). A name this product does not enrol is no agent and
no stand-in: every write is refused (AGENT_NAME_REFUSED), naming the name that came and the form accepted, and
nothing is made, your own key and an identity included; in the hook it is the recording that is not done. A --which that names another than the record’s name is not recorded where the agent
signs, and a line on the error stream says so; mnema memory and mnema observe with the variable and no
--scope go to the private tree, where a CI runner has no key that enrols the agent: the write is refused.
The hooks of corrections sign with your key unless MNEMA_AGENT_KEY is in their environment (the manifests
pass it to the server only).
In Claude Code, VS Code and Cursor, which read one manifest, the plugin’s agent_key option is empty by
default, and in Claude Code an empty option leaves a MNEMA_AGENT_KEY of the host’s environment in force (that the option, and MNEMA_AGENT_KEY, reach the server was not measured on VS Code or Cursor); the manifests of Codex, Copilot CLI and Gemini CLI name their own host; the hook files of
every host whose hooks are processes pass the host’s name too. mnema doctor lists the agent keys
this machine holds against the record of each tree (the committed, the private and the global one) and, on a machine that
holds an agent key already, names the agents the files generated for each host ask for that no key is held under; it says
too when MNEMA_AGENT_KEY, in the environment of the doctor itself and not the host’s, names one it does not hold.
For a host with no manifest of this plugin, put --agent-key <name> in the host’s user configuration, not in a
project’s: a host’s project configuration is what the model writes, so the choice of key does not go there.
mnema show <id> marks a decision’s recorder or ruler that an agent key signed (agent claude, attested by its own key), and mnema audit accountability says, beside an author, claude 40 (38 attested) for an agent whose
key signed some of its facts; with --json each agent carries how many of its facts the record’s own agent key
signed (attested, absent where none). That is read from the index, as show reads everything; mnema verify
is what judges it.
What mnema verify holds an agent key to. The key is a member of the identity that enrolled it,
and what it signs is refused unless all of this holds: its who is that identity’s anchor; its
which is, byte for byte, the name the key was enrolled under; and its kind is not one that
changes who may sign or what a key may sign (identity.founded, key.enrolled, key.revoked,
backup.declared, checker.enrolled, checker.retired, agent.enrolled, agent.retired,
account.linked). A key already in an identity, holding a role, or retired from one, cannot be enrolled as an agent.
A name may not begin or end with a character of the set FORMAT.md section 6.2 writes out, and may
not be the anchor itself. The Python reader in packages/chain/verifier judges the same, and the two
are tested to agree.
A retired agent key signs nothing. Only the identity that enrolled the key retires it, by a key
of its own that is not an agent key, and the retirement counts only once a checkpoint covers it. From
then on verify refuses any event the key signs. What it signed before still verifies, and the census
names it, as it names a retired checker’s results (census [retired-agent] …, with and without --verbose):
a leaked key can date a fact before its own retirement, so the record no longer vouches for those facts.
What the census says, and what it does not. A fact that verify accepts from an agent key is
counted per key (census [attested-which] …: that key, enrolled as that name, signed that many
facts). A which written by a key without the role is counted as declared (census [declared-which] …, shown with --verbose): the key that signed stated it, and nothing attests it. All three are informational:
none moves the verdict or the exit code. The record does not say which process held the agent
key, nor which model drove it; that is a property of the machine, and a program with access to the
key signs as that agent.
Reading the git log against the record
Section titled “Reading the git log against the record”A commit carries the change and the record carries the reasoning; a trailer in the commit’s
message is the line between them, and four verbs read it (a fifth, commit-hook, is below). They run git log when asked, take no
lock and write nothing: not a pointer to a commit, not a count.
# The line to put in a commit message. It carries the label, or `--id` for the id, which names# the decision in whichever clone it is read.git commit --trailer "$(mnema trailer ADR-4)"
# The commits that cite a decision, and, as a second list, the commits that touched a path it# addresses.mnema commits ADR-4
# Under which decisions a file stands, or a commit: the rules in force that address it, each# saying what it does there (governs, refuses a write, asks for a person), and the commits that# touched it and carry a trailer. A rule the project inherits is listed too, naming its origin.mnema why src/billing/invoice.ts
# Accepted decisions whose addressed paths changed in many commits since they were accepted.mnema aging --min-commits 10What it proves. That a commit’s message carries the trailer, and that a commit touched a path
a decision addresses: facts git holds and anyone can re-run with git log. What it does not.
A trailer is a claim its author made, signed by nobody: it does not say the commit follows the
decision, and the record does not know the commit exists. A commit that touched a path says only
that the change landed where the decision speaks, and the two lists are kept apart for that
reason. why answers with the rules in force today, not the ones that were when the commit was
made. A clone that holds part of the history says so, and answers from what it holds; a commit
rewritten away takes its trailer with it, and the record does not keep a copy. aging counts one
number (commits since the acceptance that touched an address of the decision, 20 or more unless
--min-commits says otherwise) and it points, it does not conclude: a decision about a file
edited every day because the decision is being followed is listed too. Only accepted decisions
that hold an address are asked about. commits and why show the newest 30 of a list and say when there were more.
A hook that suggests the trailer, if you want one.
mnema commit-hook install # writes the prepare-commit-msg hook in this repositorymnema commit-hook uninstall # removes it, if mnema wrote itOnly these two verbs touch your hooks; mnema init does not. The hook goes where git reads hooks
(core.hooksPath when it is set, otherwise the repository’s hooks/), and if a prepare-commit-msg
is already there that mnema did not write, install refuses and prints its path; uninstall removes
a file only when it is byte for byte the hook install writes. When you commit in an editor, the hook
appends a few # lines to the message naming the decisions in force that govern the staged files
(the reading that finds the rules for a path), each with its Mnema-Decision: line. Git drops # lines, so a
commit you leave alone is the commit you would have made; to cite a decision, delete the # in front
of its line. What it does not do. It suggests and never decides: it does not check that the change
follows the decision, and it adds nothing to a message given with -m or -F, to a merge, an amend, or
under a commit.cleanup that keeps comments or a core.commentChar other than #. A --cleanup=verbatim
typed on the command line is not seen by the hook, and the block stays in the message, like git’s own
comments. It needs mnema on the PATH git runs hooks with; when
mnema is missing or fails, the hook does nothing and the commit goes through. It looks for the
project from the repository’s top directory, so a record kept in a subdirectory draws no suggestion.
A record that inherits another repository’s decisions
Section titled “A record that inherits another repository’s decisions”A project can read the decisions in force of another repository (a team’s shared conventions, say)
without copying them in. mnema inherit set <origin> --at <revision> --write writes
.mnema/inherit.json, a committed file naming the origin and one commit of it; mnema inherit update moves that commit and prints what changes first. Without --write, both print what would
be inherited and write nothing.
mnema inherit set ../team-decisions --at mainmnema inherit update --to main --writemnema brief then prints the inherited decisions in force in a section of their own, naming the
origin and the commit. They are read and never written: this project does not sign them, its
verify does not count them, and nothing is added to its chain. What it proves is that the
record at that commit verifies with the same check as the project’s own, and a record that does
not verify there contributes nothing, and the brief says so. What it does not prove is that the
origin is the one you meant or that it deserves trust: pointing at an origin is trusting it at that
commit, and the commit never moves unless inherit update --write moves it. Only set and update
use the network; a read uses this machine’s copy, kept under the mnema home and not in the project,
and says when the commit is not there yet.
An inherited rule reaches the path it addresses. A link the origin’s record holds under
governs, asks-for-a-person or refuses-a-write is an address in the project that inherits it,
read against that project’s root, by path segments, the way the project’s own are: a rule of the
origin addressed at packages/php/src covers packages/php/src/Foo.php here, and in a project with
no such directory it covers nothing until a file is written under it. So mnema rules <path>, mnema why <path>, the governing_rules
tool and the gate before a write (the MCP tool Claude Code’s hook calls and mnema before-a-write)
list an inherited rule beside the project’s own, and each inherited rule says where it comes from
(inherited from <origin> at <commit>, the first 12 characters of the commit). Neither is chosen
over the other: where a rule of the project and a rule of the origin address the same path, both are
listed. A rule of the origin that refuses a write or asks for a person applies as one of the
project’s would, only where it addresses the path, and the refusal says the rule is addressed here
by the record this project inherits and names its origin. Where a refused rule is held only by the
origin’s record, the refusal says so and names the clone it opens in (mnema show <id> opens it in
a clone of <origin> at <commit>), because mnema show <id> here finds nothing. The
channel.refused or channel.asked this project records cites the rule’s id and nothing more: not
that the address was inherited, nor the origin and the commit.
What is inherited is the address. A line is labelled by the record that writes the link (the
address), not by the record that holds the rule. So a link the origin’s record holds to a rule of
the project (mnema link <project-rule-id> README.md --rel refuses-a-write, run in the origin)
means: the rule is the project’s, the address is the organization’s. Every line it reaches, the
refusal, the asking, mnema rules and governing_rules, says inherited from <origin> at <commit>,
and the rule still opens here with mnema show <id>, which lists only the addresses the project’s
own record holds.
An address of the origin that names nothing in this project’s
working tree is not counted as one of the project’s stale addresses. The three count lines of
mnema rules count “in this project” the project’s own record alone (an inherited row that covers
the path is still counted among those that govern, ask or refuse “here”), and one more line counts
the inherited addresses by relation (inherited from <origin> at <commit>: N addresses (G govern, A ask for a person, R refuse a write), M name nothing in this project). mnema brief counts, after
the project’s own three, the rules the inherited record places at an address (how many govern, ask
for a person and refuse a write), and says the counts before it are the project’s alone. A record that does not verify at the pinned commit, or that this machine has not fetched,
governs nothing here, and rules, why and governing_rules say so in one line; the gate says
nothing for it. The switches that turn a channel off are the project’s own: the origin’s are not
read. The verdict is kept for the life of a process, so the server a host keeps up verifies the
inherited record once per connection; a project with no inherit.json pays one file test. mnema rules-file, the commit hook and mnema aging do not read the inherited record, and mnema show
does not list the addresses it holds.
Is mnema set up on this machine
Section titled “Is mnema set up on this machine”mnema doctorOne line to a finding, each with what to do about it: whether a mnema is on the PATH and which
one runs, whether the first node on the PATH takes it (a hook starts mnema through #!/usr/bin/env node)
and, where the Node Cursor’s command-line agent carries is on this machine, what it leaves the plugin’s hooks, whether the Claude Code plugin is installed and at what version, whether the MCP server is
declared more than once (a session is offered every tool once per declaration), and whether a second
mnema, or an npm package of that name, is installed (and, when the first one on the PATH is a
script that shadows the other, which). It also says whether VS Code’s user settings.json lists the
plugin under chat.pluginLocations (the setting VS Code’s agent needs, marked experimental there)
and which projects of ~/.claude.json (the one inside $CLAUDE_CONFIG_DIR when that is set) still declare the server, those whose directory no longer
exists included (claude mcp remove does not reach them; the line says what to delete). It reads
files on this machine and, asked alone, writes nothing; it does not ask the registry, so a package
that is published and not installed here is outside what it can say.
Where this machine holds an agent key it also says, for each, in which of the trees it reaches the record enrols
it, and says when MNEMA_AGENT_KEY in the environment of the doctor itself names one this machine does not hold, and
when your own key is still unprotected while an agent key exists.
In a project it also names each macOS metadata file (keys/._<name>.pub) in the committed record: both verifiers skip
it as they skip .DS_Store, and the line says you can delete it.
In a project inside a git repository it also names each worktree that still holds private notes
in .mnema/private/, where they lived before the private tree moved into the repository’s git
directory: nothing reads them there, and removing that worktree deletes them.
It also says when the project lacks what mnema init leaves beside the record, which is the .mnema/README.md, the
.mnema/.editorconfig, or the line .mnema/ in a .prettierignore where prettier is declared. It writes none: each
finding says to run mnema init, which adds exactly that. (--fix takes a named target, and these are not among them.)
Asked of git, in a repository, it says two more things about the page mnema site writes under the record: that git would
not ignore .mnema/site/index.html (where .mnema/.gitignore lacks /site/ it says to run mnema init, which adds it;
where the line is there it says a ! rule re-includes the page, and init changes nothing), and that git already
tracks files under .mnema/site/, with their count and the command to stop tracking them, git rm -r --cached .mnema/site, which the doctor never runs. Outside a repository, with no git on the PATH or when git does not
answer, it says nothing of either.
It says, too, each file an agent host reads that lacks this version’s mnema instructions (one with none, an earlier
version’s block, or a block somebody edited), with what mnema instructions would do about it. Of AGENTS.md and the
three files of mnema’s own it looks at the ones that hold the instructions, which is how it knows which you chose, and
says they are missing only when none of the four does. So with two or more chosen, taking the block out of one of them
is not reported; with only one chosen, taking it out is, and doctor then names all four.
And it says where another tool would rewrite the record, because a formatter that changed one byte of it breaks its
signatures: a .pre-commit-config.yaml with a hook that rewrites files (end-of-file-fixer, trailing-whitespace,
mixed-line-ending, pretty-format-json, prettier and the like) that can reach a file of the record, judged by the
files, exclude, types and exclude_types of the hook and of the file (a formatter of Python never warns, and an
exclude counts only if it takes the root .mnema/ out); a biome.json
or biome.jsonc whose files does not leave .mnema out (ignore in Biome 1, includes with a ! in Biome 2); a
dprint.json without .mnema in excludes; and a lint-staged configuration (in package.json or a
.lintstagedrc*) with a glob that reaches the folder (*, **/*, *.json) running a command that writes. Each
finding names the file and the line to add, and mnema init edits none of those files: they are yours. The
configurations are read by their JSON or their lines, so a form this does not read (a script, an unusual YAML) is
reported as one that could not be confirmed, with the same line to add. It looks in the project’s directory and in
each one above it up to the git root.
mnema doctor --fix vscode is one of the two things it writes, and only when you type it: it shows what it
will change (--dry-run stops there), copies settings.json to a file beside it, adds the plugin’s
folder in the Claude Code marketplace to chat.pluginLocations and drops an entry that points into
a versioned folder of the plugin cache, keeps the file’s comments and the rest of its text, and a
second run changes nothing. A file it cannot edit safely it refuses, with the reason, and leaves
untouched. mnema doctor --fix private-tree is the other: it moves each of those tails, as it is,
into the repository’s private tree (--dry-run says what it would move), drops the writers’
locks/, and leaves where it was anything whose name there already holds other bytes. A worktree
that had written again since keeps the tail it writes now, and its old tail is moved as a closed
one. The exit status is 0 whatever it found: a script that wants to act on a
finding reads the line.
Skills and agents a project adopts: stacks
Section titled “Skills and agents a project adopts: stacks”A stack is a folder somebody publishes so another project can take it whole: a stack.json,
skills in the open Agent Skills format, agents in a neutral format, and a licence. Its contract,
its validator and the digest it is known by are @mnema/stacks.
mnema stack add ../evidence-first --dry-runmnema stack listmnema stack checkmnema stack add reads a folder, a tar archive or an https:// git address and shows the plan
whole: the files, the folders of each host that receive them, the hooks the stack declares (apart,
and off) and the digest. It writes nothing until --expect names that digest, and it refuses a
stack that brings an MCP server. The adoption is a signed fact, stack.adopted, in the tree
--scope names: public by default, so the files are committed with the project. --to <folder>
writes the files and records nothing. Skills go where Claude Code, VS Code, Cursor, Codex, Copilot
CLI, OpenCode, Gemini CLI, Antigravity, Factory Droid and Qwen Code look for them, and agents where
Claude Code, VS Code and Cursor do; each of those folders is read in the host’s documentation, not
measured. mnema stack remove keeps every file changed since it was written, and mnema stack check
fails when the files or their receipt part from the record. A hook a stack declares stays off until
mnema stack enable shows its script and a person at a terminal types its name.
After it writes, the install asks skills-ref (the validator the Agent Skills specification
publishes; its command is agentskills) about each skill folder it wrote and prints one line each,
skills-ref accepted <folder> or skills-ref refused <folder>, with what it said. It does this only
when agentskills is on the PATH; otherwise it prints that skills-ref did not run. It is never a
dependency of mnema, a refusal from it does not take the files back, and what it said is not recorded.
What it proves is that the files written are the bytes the digest names and, when the stack
carries stack.sigstore.json, who signed that digest, checked offline. What it does not prove
is that a stack is safe to run, or that its instructions make an agent better: a signature says who,
not what. Any key that writes to the tree can record a removal, whoever adopted the stack.
When mnema itself fails
Section titled “When mnema itself fails”An error in mnema’s own code is not a refusal, and it says so: the command names it as an internal
error, keeps a short note of it on this machine (outside the record) and points at mnema report. What a
person or their environment handed over is not mnema’s fault: a failed network call, or an address
that is not an address (the one a CI job gives for its signing token, say), is said as a refusal
with exit 1, and no report is offered for it.
mnema reportIt shows, whole, the report it would make of the last one: mnema’s version, Node’s, the platform,
the verb, the kind of error and where in mnema it happened: no record content, no path, no name, no
address, no message, and a report that would carry one is refused whole. It sends nothing. At a
terminal it prints a link that opens GitHub’s issue form with the report filled in, and nothing
leaves until you press Submit there. --decline stops offering reports of that kind of error, and
--off switches reporting off on this machine. An agent that meets the error is told to leave the
report to you: no tool of the MCP server sends one or accepts one.
The exit codes
Section titled “The exit codes”A script reads mnema’s exit code, so each one has one meaning. These are all the numbers the
code sets; a shell, a signal or the host can end a process with another, and that is not
mnema saying anything.
| Code | Who ends with it | What it says |
|---|---|---|
0 |
mnema |
The command ran and nothing it was asked to rule on failed. |
1 |
mnema |
A refusal: what was asked was not done (a usage error, a write that was refused, a verify that found a break or did not reach the level --require asked for). It is the person’s or their environment’s to act on. |
2 |
the SubagentStop hook (plugin/hooks/subagent-stop.mjs), never the command line |
The hook sends a subagent’s reply back with the reason on the second stream, which is how Claude Code reads a block; once per subagent. |
70 |
mnema |
An internal error: a fault in mnema’s own code, not a refusal. It is named as one, a note of it is kept on this machine, and mnema report shows what it would send (above). |
127 |
the plugin’s launcher of the MCP server (plugin/server/launch.mjs) |
It could not start mnema. Where it started it, the launcher ends with the code the server ended with. |
mnema verify --json has the same exit codes as the prose reading (the fields are described with verify, above).
What goes into the record
Section titled “What goes into the record”Every field of text you record passes one door on the way in.
mnema memory "the deploy key is AKIAIOSFODNN7EXAMPLE and the db is postgres://svc:hunter2@db.internal/app"#> Captured memory 019fa920-9f32-7dcc-8632-252878a942d9#> 2 value(s) replaced before recording: <SECRET:aws-access-key>, <SECRET:url-password>#> This record is permanent. If those were real credentials, rotate them.What landed is the sentence with two placeholders in it: the scheme, the user, the host and the database all survive, because the useful part of the note was never the secret. Nothing is replaced in silence: the reply says what went and tells you to rotate, which is the only remedy an append-only record leaves.
That is what happens in a body. In a name the door refuses instead:
mnema skill create "xoxb-123456789012-abcdefghijkl" --body "never mind"#> Refused (NAME_HOLDS_A_SECRET): "name" reads as slack-token, and it is a name the#> record is addressed by: so replacing it would record a different entity, not a#> redacted one. The fact was not recorded. Name it something else; if the value#> itself matters, rotate it.Replacing means different things in the two. A body with the secret taken out is still the fact: the decision still says what was decided. A name with the secret taken out is not that thing redacted, it is a different thing wearing its id: nothing can look it up, nobody can name it back, and the record is append-only, so it stays that way. The fields that work this way are the ones the record is addressed by: a task’s or decision’s title, a skill’s name, an observation’s topic and what it is about, either end of a link and the relation between them, the agent and the run stamped on every event of a session. The refusal names the class and the field and never the value, and it happens before anything is written.
These are the formats the door recognizes, in the order it applies them, with what takes the place of each:
| Replaced by | What it matches |
|---|---|
<SECRET:aws-access-key> |
An AWS access key id: AKIA, ASIA, ABIA or ACCA, then 16 or more capitals and digits. |
<SECRET:github-token> |
A GitHub token: ghp_, gho_, ghu_, ghs_ or ghr_, then 36 or more letters and digits. |
<SECRET:anthropic-key> |
An Anthropic key: sk-ant-, then 20 or more letters, digits, _ or -. |
<SECRET:openai-key> |
An OpenAI-style key: sk- or sk-proj-, then 20 or more letters, digits, _ or -; a value whose every piece is a lowercase word is read as a name and left alone. |
<SECRET:stripe-key> |
A Stripe key: sk_live_, sk_test_, rk_live_ or rk_test_, then 20 or more letters and digits. |
<SECRET:slack-token> |
A Slack token: xoxb-, xoxa-, xoxp-, xoxr- or xoxs-, then 10 or more letters, digits or -. |
<SECRET:google-api-key> |
A Google API key: AIza, then 30 or more letters, digits, _ or -. |
<SECRET:npm-token> |
An npm token: npm_, then 36 or more letters and digits. |
<SECRET:jwt> |
A JSON Web Token: three dot-separated base64url parts, the first beginning eyJ. |
<SECRET:private-key-block> |
A private key in PEM form (-----BEGIN ... PRIVATE KEY-----) or mnema’s own protected key file, the whole block when it is closed, only its header when it is not. |
<SECRET:url-password> |
The password of scheme://user:password@host; the scheme, the user and the host stay. |
<email> |
An e-mail address, except noreply@, no-reply@ and GitHub’s noreply.github.com form, which name no one. |
Three limits, all stated rather than hidden. It catches only formats it
recognizes: a proprietary token or a password in prose goes in verbatim, so
the placeholder is damage reduction and not a licence to paste secrets. A value
that reads as a name is left alone: sk- is a prefix several key issuers
reuse and also the two letters you reach for when you name a skill, so a value
whose every piece is a lowercase word is recorded the way you typed it; measured
against thirteen key formats that costs nothing, 0 of 260,000 draws stopped being
caught, and what it does cost is a name written in camelCase, because an
uppercase letter is the mark of a key drawn at random. And a single field holds
at most 64 KiB: a longer one is refused, never truncated, so nothing is
dropped without your knowing.
For everything written before that door existed:
mnema audit exposure#> 1 of 4 record(s) hold a credential format:#> public 2026-01-01 memory.captured 019fa8b7-0410-717b-9af2-cfeb013fc4ac aws-access-key, url-password#>#> These records are permanent: nothing deletes a fact. Rotate the credentials.#> A public record is committed and on every machine that cloned the repository.It reports where, never what: the id, the kind, the tree, the instant and the class. Printing the value would move the credential into your terminal scrollback and your CI log, so the report has no field that could hold one.
The decisions you already wrote
Section titled “The decisions you already wrote”Your project probably decided things before it had a record, and wrote them down.
mnema decision import reads a directory of decision documents and proposes each
one. Nothing is accepted on your behalf: every proposal is born proposed, whatever
the file says its status is, and you rule on it with decision move accept, or one by one at a
terminal with mnema decision review (it asks for one note, then accept, reject, skip or quit for each
proposal, signs with your own key, and without a terminal writes nothing and prints the move command).
Run it without --write first. Nothing is written, and the plan tells you what the
door made of every file:
mnema decision import docs/decisions#> 3 proposal(s), 1 refused: nothing written. Add --write to record them.A file whose decision the record already holds (an earlier run drew it from that file) is listed
as already in the record, unchanged. So is a file that says exactly the words of a decision
already recorded (a copy under another name, or two files in one run): nothing is written for
it, the line names the decision that holds those words, and no derived-from is drawn from
that file, since the decision was not born from it. Such a file is reported the same way on
every later run.
Other places decisions are written are read with --format, down the same road: planned
before written, every one born proposed, cited to where it was read, and the reason of each opens with
a line saying it was imported as written there and is not yet the project’s own words.
--format |
<source> is |
What is read |
|---|---|---|
ecc-vault |
a directory of the ECC Memory Vault’s *.json memories |
kind: "decision" only; a rejected or superseded state is skipped |
rulings |
one ledger file | each line starting Ruling:, cited as file:line |
claude-memory |
a directory of the memory files Claude Code writes under ~/.claude/projects/<project>/memory/ |
the name: frontmatter as the title, the body as the reason; MEMORY.md is skipped |
A source outside the project is cited as <format>:<file>, a name no clone can open. A file that
does not have its source’s shape is named and nothing is read from it. A rulings origin
carries a line number, so a ledger edited above a line is read as new.
The shape it reads is two things. A level-1 title, and a why: the file’s own decision
(the Chosen option, because sentence of a MADR ## Decision Outcome, or the ## Decision
of a Nygard record) when it has one; else a ## Context section (## Contexto is read too)
or prose directly under the title. The context is the situation and not the reason, so a
file that states a decision is recorded with the decision. Everything else (the status, the
alternatives) is read when it is there and absent when it is not: a MADR ## Considered Options lists every option, the chosen one included, so it is recorded without the option
the file chose, and when the file does not say which it chose, none is recorded as turned
down and the plan says so. This is the smallest file that goes in:
# Store timestamps in UTC
## Context
Three services send us times in three zones and the comparisons were wrong.What it will not read, and what it cannot tell. A file with no title, or with no
prose it can read as a why, is refused BY NAME with the reason beside it: a rule like
--- under the header is punctuation, not a reason, and is refused as one. But a file
this CAN read is proposed even when it is no decision: an index page, a roadmap and a
DECISIONS.md holding a hundred rows all have a title and prose under it, and no fact
of the document separates them from a decision. That is why nothing is accepted for
you, and why the dry run above is worth reading before you add --write.
If your decisions are in one file
Section titled “If your decisions are in one file”The door takes one decision per file, and it will not learn your table: a generic table reader would still need you to declare which column is the title, which holds the why, and which section of the file is history rather than agenda. Declaring all that is writing the converter, in a worse language. So write the converter: it is about thirty lines, it runs once, and what it produces is checked by the dry run above.
// convert-decisions.mjs: one decision document per row of a markdown table.import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';import { join } from 'node:path';
// EDIT THESE THREE. Nothing can work them out for you, which is the whole reason// this is a script you own rather than a flag on a verb.const SOURCE = 'DECISIONS.md'; // the file holding the tableconst OUT = 'docs/decisions'; // where the one-per-file documents goconst COLUMN = { title: 1, rationale: 2 }; // which column is which, counting from 0
const cells = (line) => line .split('|') .slice(1, -1) .map((cell) => cell.trim());const says = (text) => /[\p{L}\p{N}]/u.test(text ?? '');
const rows = readFileSync(SOURCE, 'utf8') .split('\n') .filter((line) => line.trimStart().startsWith('|')) .map(cells) .filter((row) => !row.every((cell) => /^:?-+:?$/.test(cell))); // the separator row
mkdirSync(OUT, { recursive: true });let n = 0;for (const row of rows.slice(1)) { // The first row is the header. const title = row[COLUMN.title]; const rationale = row[COLUMN.rationale]; // A row with no word in either cell is not a decision, and the import would refuse // it by name anyway: better to know that here, where you can look at the row. if (!says(title) || !says(rationale)) continue; n += 1; const slug = title .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') .slice(0, 60); const at = join(OUT, `${String(n).padStart(4, '0')}-${slug || 'decision'}.md`); writeFileSync(at, `# ${title}\n\n## Context\n\n${rationale}\n`);}console.log(`${n} document(s) in ${OUT}, from ${rows.length - 1} row(s) of ${SOURCE}`);Then look at what it wrote, and let the dry run rule on it before anything is recorded:
node convert-decisions.mjs#> 111 document(s) in docs/decisions, from 118 row(s) of DECISIONS.md
mnema decision import docs/decisions#> 111 proposal(s), 0 refused: nothing written. Add --write to record them.Read the refusals before you write. A count of proposals with refusals beside it is
the only check there is that your column mapping was right: a table whose rationale
column is really a link column converts cleanly and proposes 111 decisions whose why is
a URL. The dry run cannot catch that. You can, by opening two of the files it wrote.
What a recorded decision offers
Section titled “What a recorded decision offers”After “Recorded decision ADR-n (id)”, mnema decision record <title> <rationale> and the record_decision tool may add lines that
say what the decision could be tied to. They only say it: nothing is linked and nothing is moved by recording, and each
line names the call that would do it.
- Which decision it could replace. When the title, the rationale or the alternatives cite another decision, by
its full id or by an
ADR-<n>label, and that decision isproposedoraccepted(the states a supersede is legal from), the lines give its label, title, state and id, and the call that would replace it:decision_transitionwithaction: "supersede"through the tool,mnema decision supersede <old> <new> --reason "..."on the command line. A label that several decisions carry (two trees number their own) lists each. At most five are named, and the rest are counted. A decision recorded in the public tree is offered only decisions of the public tree: a title out of the private tree is never put in reach of a reply that may be quoted into a record that travels. One recorded in the private tree is offered both. - Which path it could be addressed at (the tool only). The MCP connection remembers, in memory and nowhere else,
the paths of the project it was asked about: the ones given to
governing_rules, and the ones the host handsrules_before_an_editbefore an edit, which only Claude Code does. The reply lists up to ten of them, the latest first, and thelink_knowledgecall withrel: "governs"that would address the decision at the latest. A path outside the project is not kept, and a restarted server starts with none. In a host that does not callrules_before_an_editonly the paths asked ofgoverning_rulesappear, and the command line, which has no connection, offers no address.
The working tree’s changes are never read to find the paths: they could credit the session with what it did not do.
Recording the same decision twice
Section titled “Recording the same decision twice”The decision record command and the record_decision tool look, before they write, for a decision of the same tree that is
proposed or accepted and whose title, rationale and alternatives are exactly the ones given (after the replacement of
secrets every write applies, so a text with a credential matches the second time too; alternatives present in one and
absent in the other differ). If there is one, nothing is written and the answer is Decision ADR-n (id) is already recorded in the <scope> tree with this title, rationale and alternatives. Nothing was written., with the existing label and id and none
of the offers below. This is what makes a call repeated after a lost reply safe. A rejected or superseded decision is not
matched: the same words recorded again are a new proposal.
Recording the same memory or observation twice
Section titled “Recording the same memory or observation twice”memory and capture_memory, observe and record_observation look, before they write, for a note of the same tree that has
not been retracted and says the same: a memory with exactly the text given, an observation with the same text, topic and
about (after the replacement of secrets every write applies). If there is one, nothing is written and the answer is Memory <id> is already recorded in the <scope> tree with this text. Nothing was written. (an observation says Observation <id> about <about> ... with this topic and text.), with the existing id. This is what makes a call repeated after a lost reply
safe. A retracted note is not matched: the same words recorded again are a new note. A task is not looked for: two tasks with
one title are two tasks.
The mnema instructions in the files a host reads
Section titled “The mnema instructions in the files a host reads”When it founds a project, mnema init writes a short block into the files agent hosts read as
standing instructions, and names each file it wrote. At a terminal it first asks which of the four files
below that it offers (AGENTS.md and the three of mnema’s own), listing the hosts that read each:
Enter writes all four, numbers write those, and Ctrl-C or Ctrl-D founds nothing and writes nothing. With no terminal
(a script, a pipe, an agent’s shell) or with --all-hosts it asks nothing and writes all four. What decides is whether
standard input and standard error are a terminal, and init looks at nothing else: no host leaves a variable in the
shell of its agent that the project has measured (see Evidence: what is measured is the environment of the MCP
server, and Codex and OpenCode leave none of the marks looked for). An agent whose shell is a real pseudo-terminal
(an editor’s integrated terminal, a node-pty) would be asked the question and wait for an answer nobody gives, so an
agent that runs mnema init there passes --all-hosts. The question comes
before the founding, so leaving it leaves the project and the home as they were, and a second init does not ask.
A CLAUDE.md, .claude/CLAUDE.md or GEMINI.md that already exists gets the block whichever you choose. The block says when to call the mnema tools
(bootstrap at the opening, governing_rules before changing a file, record_decision when a choice
is settled, capture_memory for what the code does not show), that a decision is born proposed and,
once accepted, is in force (an agent’s acceptance is recorded as an agent’s, and mnema switch off agent-accepts
leaves it to a person), that a subagent hands its decisions back rather than recording them, the
mnema decision record command where the tools are not connected, as an example that runs as it
stands (with --alternatives), how a decision that replaces an earlier one says so
(decision_transition with action supersede, or mnema decision supersede <old-id> <new-id> --reason "<why>"; the new decision is then read as replacing the earlier one, which read_record, show and the opening name), and what never goes in the record. It applies only where the mnema tools are connected, so a fork without mnema reads it as
inert, and it carries nothing of the record: the decisions in force reach a session as it opens,
through the plugin.
| File | Written | Read by, as each host documents it |
|---|---|---|
AGENTS.md |
offered by init, created where there is none |
Codex, Cursor, VS Code, Copilot CLI, OpenCode, Qwen Code; Claude Code only where no CLAUDE.md exists |
CLAUDE.md, .claude/CLAUDE.md |
where they exist | Claude Code, Copilot CLI |
GEMINI.md |
where it exists | Gemini CLI, Copilot CLI |
.claude/rules/mnema-instructions.md |
offered by init |
Claude Code, at launch |
.github/instructions/mnema-instructions.instructions.md (applyTo: "**") |
offered by init |
VS Code and Copilot CLI, when the agent works on a file |
.cursor/rules/mnema-instructions.mdc (alwaysApply: true) |
offered by init |
Cursor |
The Gemini CLI extension carries the same text as its own context file. Every copy is the same
text, so a host that reads several (Claude Code reads its CLAUDE.md or AGENTS.md and the rule
file; Copilot CLI reads them all) reads it more than once rather than two versions of it. The text
is 2,390 characters (the one before was 1,721).
mnema instructions # write the block where it is missing or an earlier version'smnema instructions --check # say what each file holds; exits 1 where one lacks this version's blockmnema instructions --remove # take it out of each filemnema instructions --all-hosts # write all four offered files, asking nothing, whichever are thereThe files on disk are the choice. Of the four offered files, mnema instructions, --check, --remove and
mnema doctor look at the ones that hold the instructions; one you did not choose is not expected, and is
reported missing only when none of the four holds them. Where none does, a terminal is asked the question
init asks, and without one all four are written. A file of yours with no block (an AGENTS.md you wrote) does not
count as chosen. If you take the block out of the one file you chose, doctor says all four are missing (none holds it);
if you chose two or more and delete the block from one, nothing says it is missing, because the others still hold it.
--all-hosts puts it back, with the others.
The block is written the way a Markdown formatter (prettier) leaves it: a blank line after the begin
marker and before the end marker, the examples as indented code. In a repository that formats its
Markdown, format:check passes over the files init writes, with one exception: prettier writes
the applyTo: "**" of the .github/instructions file with the quotes your own configuration
asks for, so with singleQuote: true that one file is reported until you run the formatter over it
(it keeps the block). init writes that applyTo with double quotes, prettier’s default. A project
with singleQuote: true can leave the file out of prettier (a line in .prettierignore) or accept the
rewrite to applyTo: '**': the two are the same YAML value, and VS Code’s own page on custom
instructions writes the field with single quotes in its example
(code.visualstudio.com/docs/agent-customization/custom-instructions). An earlier version’s block
(the begin marker names it, v2) is replaced by the current one. A formatter that re-wraps prose
(proseWrap: "always") moves line breaks and spaces inside a paragraph or a list item and nothing
else, and the block it leaves still counts as mnema’s (checked against prettier with always,
never and preserve); a word changed, a line taken out or two lines run into one is your edit: it
is left as it is, and said.
The block sits between <!-- mnema:begin v3 … --> and <!-- mnema:end -->, after what your file
holds, and nothing outside the markers changes; a file the block was all of is deleted by
--remove, which takes out with the block the one line break that went in with it only where the
block is the last thing in the file (a line you wrote after it is never glued to the one above), and
says “as it was before them” only when putting the block back would give the file it found. A block
whose text is not one mnema wrote is your edit: it is never overwritten, said, and the run exits 1.
The same for a symbolic link that leaves the project or leads nowhere (the line says where it goes),
and for a file that cannot be read or written (a directory named AGENTS.md, a read-only
CLAUDE.md): that file is left as it is, said with why and the command to try again, the others are
still written, and the run exits 1; mnema init prints the same lines and still founds the project.
A link that resolves inside the project is followed. Line endings are kept: a block whose lines a
checkout turned into CRLF is still the current one, and a rewrite uses the file’s own. Markers shown
inside a fenced code block (a README that documents them) are an example, not a block. A block you took out stays out: init writes the instructions only when it founds a
project, and mnema doctor says where they are missing. The block an earlier, alpha version of mnema
wrote between <!-- MNEMA:START --> and <!-- MNEMA:END --> is not recognized: it stays byte for byte
where it is, --remove leaves it, and the block above is added beside it.
Handing the record to an agent that never asked for it
Section titled “Handing the record to an agent that never asked for it”Reading the record is something an agent has to think of doing. What a session is
handed as it opens is not. The plugin hands this document to every
Claude Code session with no file in the repository at all, and beside it the notes
recorded here (mnema recall, below). Where the plugin does not reach, mnema brief prints what governs the work as markdown, and where the file goes is your choice.
Which file a host reads is the host’s to say, and this page used to say it for it.
It read “AGENTS.md is an open convention several hosts read natively, and
CLAUDE.md is read when a session opens”, as if the two were read alike. Claude Code
reads a CLAUDE.md at every session, and an AGENTS.md only from 2.1.277 and only where
no CLAUDE.md exists in the working directory or above it: measured on one machine, not
one of 299 sessions loaded an AGENTS.md, in two projects that keep both files. A
sentence in a CLAUDE.md naming another file is read only if the agent chooses to open
that file; the line @MNEMA.md is an import, and brings the file in with it.
The redirection replaces the whole of the file it names. That is the shell’s doing
and not this verb’s (brief records nothing and never learns there was a file), so the
example below names a file nothing else claims. Where your host’s own file exists it is
somebody’s own method, and this document would replace every word of it: keep theirs,
and let one line in it bring the file below in.
mnema brief > MNEMA.md#> (nothing: the document went to the file)
mnema brief#> <!-- Generated by `mnema brief` from this project’s mnema record. Do not edit by hand. -->#>#> # What governs the work here#> …#> It carries what is COMMITTED to this project: the record a clone of the repository#> gets, and nothing kept privately on one machine or for one person. A rule recorded#> that way is not below, and each heading counts what is printed under it. Recording#> changes `.mnema/`, and that change goes in the same commit as the work it describes.#> …#> 1 of the rules below has an ADDRESS: a path in this#> repository, recorded beside the rule. The rules addressed at a file arrive on their own#> at each edit, beside the result of that write, and not for a file none of them names.#> Ask `governing_rules` with a path for the whole answer about it.#>#> ## Decisions in force (2)#>#> Each was accepted, and none of them superseded. For the argument behind one, ask#> `read_record` for its id.#>#> 241 more decisions are recorded here and awaiting a judgement, and none is below.#> Nothing waiting governs anything yet; ask `bootstrap` for what is waiting.#>#> - **ADR-2: Keep the runbook in the record** · `019faa06-30e1-7a41-9c05-3d8e6f2a1b44`#> - **ADR-1: Rotate the credentials every quarter** · `019faa06-335f-7b02-8e11-6c2fa9d41e07`Each heading says how many are awaiting a judgement: recorded here, ruled on by
nobody, and not in the list under it. A heading that counted only what it printed was
true and left the wrong impression: ## Decisions in force (6) over a record holding
247 decisions reads, to the agent this file is written for, as a project that decided
six things. Zero says so in words, for the reason every other empty thing here does.
The verb writes nothing: the redirection is yours, so the file stays a file you own: mnema has never written outside its own tree, and a generated file with two owners is a file with a merge conflict in it.
The document holds no clock, no session and no path, so the same record always prints the same bytes. That is what makes the check a pipe:
mnema brief | diff - MNEMA.md#> (no output: the file still says what the record says)A difference is either a copy that fell behind or an edit somebody made by hand; either way the fix is to generate it again. Nothing here reads your file, so there is no flag to remember and no guess about where you keep it.
It carries the rules and the names (a decision by title and ADR-<n> label, a
pattern by name) and neither body: mnema show <id> is the argument behind a
decision and the text of a pattern. It carries no work list, deliberately: a
queue changes by the hour, and a copy of one in a hand-regenerated file would be
wrong between two runs, which is the one thing this record exists not to be. The
counts of what is awaiting a judgement are not that: a count over the record moves
only when the record does, where the names in it move by the hour, and the names are
mnema status --actor <id>. And the file is never cut by size: a rule missing from
the file is a rule the agent does not follow. The copy the plugin hands a session is
mnema brief --hook, and that one is: past 10,000 characters the host replaces a hook’s
whole text with a file path it does not ask the model to open, so --hook stops at a
whole rule inside that and ends by saying how many it left out and which read serves them.
What it does leave out is the record that does not travel. A decision or a pattern
recorded with --scope private, or in your machine-global tree, governs your own work
and is not in this file: the file is written to be committed, and the private tree
exists precisely so that what is in it stays here. Two consequences worth knowing:
the ADR-<n> label is numbered inside one chain, so committed-only is what keeps
folding the trees from printing two ADR-1s, and a difference against your copy
now means one thing only: the copy is stale. The document declares the scope in its
own text, so nobody reads it as the whole of what governs. For that, ask the opening
context, which spans every tree you can see: mnema status --actor <you> on the command
line, or the bootstrap tool over the MCP server: one derivation behind both.
One chain is not one machine, though, and that is the limit of the sentence above.
The number is frozen from the writer’s view of the chain, so two people who decide
while apart both mint ADR-7, and the labels meet when the branches do: legitimately,
with nothing to refuse and nothing to renumber. When a label in this document is
answered to by more than one rule, the document says so above the list, names every
id that carries it, and tells you to cite by id. mnema audit antipatterns reports the same
thing about the whole record, chain by chain, without anyone generating the file.
The notes a session opens with
Section titled “The notes a session opens with”Beside the document, the plugin hands a session the notes recorded here: the memories and the observations, out of every tree this machine holds for the project: the committed one, this machine’s own, and your personal one. It is the one text this product pushes that carries the private tree, and that is why it is not part of the document: a note an agent records lands in the tree that does not travel, and the document is written to be committed. Until it existed, what an agent noted here reached no later session unless that session went looking.
mnema recall#> # What was noted here#> …#> ## Memories (2)#>#> - The staging database resets every night at 03:00 UTC · `01a0ca93-8ba2-7000-…`#> - pnpm build must run before typecheck, or tsc reads a stale dist · `01a0ca93-8aef-7000-…`#> …Each note is one line (a memory by the start of its content, an observation by its
topic) with the id mnema show <id> reads whole, cut where mnema search cuts, and
saying how many there are when there are more.
The notes near the work come first. As a session opens nobody has asked anything yet,
so what it is near is read from the project itself, with no model: the files changed in
the working tree, the tasks in progress, the name of the branch, and the files of the last
three commits: the record’s own directory left out of both git reads, and git asked to
take no lock. A note that shares a word with those is ranked by the same index
mnema search ranks by, and comes first: a word more than half of the notes hold tells
them apart from nothing and is left out; the rest of the list is the newest. With nothing
to read (a clean tree outside git, say) the list is the newest, as it was. The text says
in one line which order it used, and under each heading how many of the notes are the near
ones. A line that still holds a
credential in a recognized format is handed over as the fact that the note exists, never
as its text. Where nothing is noted it prints nothing, and a session there is handed
nothing. It is never a file to commit: it carries what was kept on this machine.
Where the last session stopped comes above the notes. When another session that has stopped (its connection to the server
ended, or its journal has been quiet for 15 minutes: a session that wrote a moment ago may still be going and is not told as the
one that stopped) left a journal on this machine in the last 24 hours (see The session journal),
mnema recall opens with a few lines read from it: how long ago it stopped, and whether it wrote to this record (a decision, a
note copied or a task moved; a session that only edited files is said to have written nothing to it), its goal (the title of the first task it moved into progress), the decisions it recorded by their
ADR-<n> label and where each stands, and what it left (files edited, files of decisions the record cites nowhere, questions
answered with no decision recorded after, tasks in progress). About a hundred tokens, ids and counts and a title the record
already holds. The hook passes the session’s own id (--session <id>, read off its payload) so a session never reads its own
journal back as “the last one”, for instance after a compaction; a session that did nothing worth saying is skipped for an
earlier one. It is its own switch: mnema switch off last-session stops the section and nothing else, and switching the
journal off leaves it nothing to read.
A note that turned out wrong can be taken back, and is not erased. mnema retract
(the retract_note tool, for an agent) takes the note’s id and a --reason, and appends
a signed fact saying who took it back and why, in the tree the note was written in. The
note leaves this list and mnema search; mnema show still prints it whole, with the
retraction above it, and mnema verify still sees both facts. An ADR-<n> label typed as the id is turned into the decision it names, which is then refused as not a note. Only memories and
observations are retracted: a decision is rejected or superseded, and a pattern rejected or
deprecated, each by its own move. Only the identity that wrote a note takes it back, with
any key of it: another identity is refused, and a retraction another identity signed anyway
is not applied: the note is still served, and mnema verify names it in a census line,
informational, not a break. The note is the identity that wrote it first: a writing of the
same id by another identity, which only a hand-made event can be since the id is minted at
the capture, is not applied and is named the same way.
A relation is a label, and a recipe’s marker is not a value. The relation is open, but a path (--rel src/a.ts, ./x, or a name that exists in the project) is the target typed in the wrong place and is refused NOT_A_REFERENCE, as is a marker a recipe prints
where a value goes (--rel "<rel>", --which "<name>").
A link recorded by mistake can be taken back the same way. mnema unlink <subject> <target> --rel <label> --reason "<why>" (the retract_link tool) names the edge exactly as
the link took it, an ADR-<n> label included, and appends a signed fact in the tree the link
was recorded in. Every read that applies a link stops seeing the edge once nobody else asserts
it, so a rule it addressed at a path no longer governs, asks or refuses there; the history of what
names what (refs, diagram, the references tool) keeps the link and marks it
(retracted by its author). Only the identity that recorded the link takes it back; a
retraction another identity signed anyway is not applied, and mnema verify names it in a census
line. The retraction follows the link to the first tree that holds a link of that edge: in this project first, and through the MCP then in the other projects of the workspace, writing in the project that holds it, which its reply names (and says when the person’s key signed it because the agent is not enrolled there). So
when another identity recorded the same edge in an earlier tree it is refused there, and nothing
points it at the later one.
Switching off what mnema hands to a model
Section titled “Switching off what mnema hands to a model”Three things arrive without anybody asking: the document a session opens with, the notes
beside it, and the rules handed over as a file is written. Each can be switched off, and
the switching is recorded, because turning something off is legitimate and turning it
off in silence is not. The list also holds two gates that hand nothing over: the pause
before a write where a rule asks for a person, the refusal of a write where a rule refuses one
(edit-refuses-a-write), and agent-accepts, which is on: an
agent may accept a decision, freely, and the record keeps which agent did, the reply says
so, and the document a session opens with marks the rule. Switching it off makes an agent’s
accept a refusal; a person’s still lands. Every channel, where it begins and the command that
flips it are in the table of switches.
mnema switch#> 18 channel(s), looked in public, private, global:#> brief-document on the document `mnema brief` prints, which a session opens with and a …#> recall-document on the notes `mnema recall` prints, which a session opens with: …#> edit-rules-push on the rules addressed at a file, handed over at each edit of it, beside …#> edit-asks-a-person on the pause before a file is written where the record asks that a person …#> edit-refuses-a-write on the refusal of a write where a rule of the record refuses one: …#> agent-accepts on an agent ruling a decision in force: …#> edit-first-write-gate off the hold on the first write of a session to a file a rule addresses: … · off until switched on: `mnema switch on edit-first-write-gate`#> user-corrections off the proposals recorded from what a person typed into a session: … · off until switched on: `mnema switch on user-corrections`#> choice-settled on the sentence handed to the agent after a question it asked the person …#> host-memory-mirror on the copy of the host’s own memory: …#> decision-file-written on the line beside a write to a file that holds decisions (an ADR, a plan …#> subagent-handback on the return of a subagent that ends its final reply without the block of …#> tool-call-count on the local count of tool calls: …#> last-session on the section of the notes `mnema recall` prints that says where the last …#> after-a-compaction on the sentence a conversation is handed right after it was compacted …#> session-journal on the session journal: …#> agent-mirror on the mirror of what a session hands to other agents: …#> session-tally on the line a session’s `Stop` hook prints: …mnema switch off edit-rules-push --reason "not while I am porting this"#> Switched edit-rules-push off#> Landed in the public tree: committed with the repository, so it reaches every clone.#> edit-rules-push is now OFF: nothing of it reaches a model. The switch that decides it#> was made by mnid:60a50d38… at 2026-08-19T11:04:07.512Z.#> Nothing was removed and nothing else stopped: the record still holds what it held, …Four things about it are decisions rather than defaults.
It is a fact of the chain, not a configuration file. A setting is not attributed, not dated, and does not travel, and a later reader of the record could never tell “no rule addressed that file” from “somebody had turned the push off that week”. As an event it carries the authorizing identity, the executing agent if one was driving, the run, the instant and the signature, exactly like a decision.
It travels by default. The ordinary switch lands in the committed tree, so the team
reads it and the opening document says the push is off instead of implying it had nothing
to say. --scope private is the switch that means this machine only, and then mnema switch is the only reading that can report it, because a committed document carrying a
fact about one machine would make the diff above report a difference that is not the
record’s.
Off wins between trees. A switch recorded privately silences the channel even while the committed record says on, because two trees share no order (an event’s instant is a clock on the machine that wrote it), so “the most recent switch across the trees” is not a comparison this product is entitled to make. The listing says which switch decides.
No channel arrives switched off except the two that start off, and there is no mnema switch tool on the MCP server.
What it does say is where the switches stand: the bootstrap reply carries one line of its own, Off on this machine:
followed by the names of the channels that are off for the project, and that only a person turns one on, with the
command. A channel a person switched on is not in it, and with nothing off there is no line.
Anyone who can write to the record can append any fact, so this is not a claim about what is
possible; it is a claim about which doors are open. An agent that could switch off what
governs its own work through the door built for agents would be an agent that opts out of
the record.
As an MCP server
Section titled “As an MCP server”Point an agent host at the mcp subcommand; it speaks JSON-RPC over stdio. If you
installed the Claude Code plugin, it already does this: the plugin declares this same
server, and registering it here as well offers every tool twice, under
mcp__mnema__* and mcp__plugin_mnema_mnema__*, fifty-four names for twenty-seven tools to an
agent that chooses by name, and hands the session the server’s instructions twice. One of
the two is enough.
{ "mcpServers": { "mnema": { "command": "mnema", "args": ["mcp"] } }}In the handshake the server sends instructions, which a host that defers tools reads
before it has chosen any: when a decision or a note is worth recording, which reads to
make before deciding, what does not belong in the record, that it is the place for what is
also kept elsewhere, and what comes back to the next session. They are the same text for every connection, because they are sent before the
server knows which project it serves, and they stay under 2,048 characters, which is
what Claude Code keeps of them up to 2.1.295 (4,096 since 2.1.296;
tests/the-agent-is-told-what-it-has.test.ts,
tests/every-description-reaches-the-model.test.ts).
The server discovers the project in a fixed cascade: --project, then the
client’s workspace roots, then (only for a client that declares no workspace roots
at all, as Cursor’s command-line agent does), the project the host’s working directory
is in, and finally the global tree when none of them names a project. A client that
declares roots and lists none (a window with no folder open) is taken at its word: it
lands on the global tree, and the working directory is never read for it. The line the
server logs when a session opens says which of these it landed by, and the server never
creates a project; only mnema init does that (tests/mcp-context.test.ts,
tests/a-client-that-names-no-workspace.test.ts).
The cascade runs again when the client says its workspace changed
(roots/list_changed), so a folder opened mid-session becomes a project the
connection can read and write. It only ever ADDS: a session stays in the project
it landed in, a root the client stops announcing is kept, and the one move the
landing can make is from the global tree into a project. bootstrap says how many
times the client has said so: including zero, because a re-read that changed
nothing and one that never happened would otherwise look the same.
The answers of search, read_record and the audit_* reads name a project by its absolute path, never by
its directory’s name: a name can mean another directory on the next connection, a path cannot
(tests/an-answer-says-the-path-of-a-project.test.ts). A write’s project takes only the path: a bare name is refused as UNKNOWN_PROJECT, with the paths to use, and nothing is written.
A write says, in its reply, the absolute directory of the project it fell in
(In the project at /home/you/work/api.), whether it named a project, followed an id (a move or a
retraction goes where the record is) or fell in the session’s own project. The name may be one the
caller carried from another connection, and the same name can mean another directory in this one, and
after a reload the workspace may hold another checkout of the same repository; the path says
which, after the write, and does not refuse it. A write that landed in the global tree says nothing of the sort
(tests/a-routed-write-says-where-it-landed.test.ts). When the server signs as an agent and the write
went to another project of the workspace whose tree does not enrol that agent, the reply adds a line
saying the person’s key signed it and the agent it names is declared, not attested
(tests/a-write-the-person-signs-for-an-agent-says-so.test.ts).
A record that two machines moved out of one state without seeing each other (the state shown is the last
move in the record’s order) is said so beside the answer, in the words the show verb uses, by read_record
of that record and by bootstrap for the project’s own records. resume, focus and the resume inside
bootstrap read the runs of every project of the workspace, so a session whose first write went to a
project its client listed second still finds its run; while no identity signs for the session, the sentence
about the connection says that actor is empty because of that.
--project is how you make certain which record a session serves:
"args": ["mcp", "--project", "/home/you/work/api"]. Without it the project is
whichever workspace folder the host happens to announce first that has a
.mnema/ (or, for a host that announces none, whichever project the directory it
started the server in belongs to), which can be a repository you were not thinking
about, answering about a record you never meant to ask. The path must be absolute (this
server’s cwd is the host’s, not your shell’s) and it must be a project; a value
that is neither is refused rather than passed over, because a server told which
project to serve must not serve another instead.
The write tools mirror the CLI verbs (same gate, same refusals), so a move an
agent cannot make is one you cannot make either. A few names do not pair up:
init and verify are yours only, bootstrap is the same opening read the command
line calls status (one derivation, two names, because an agent’s opening call and
what a person types are not the same word), and skills exists on both sides doing
different things.
bootstrap is an INDEX, and every list in it names the read that serves the rest:
the LIVE work by name (next_actions for the moves one allows), the adopted
patterns by name (skills for the pattern itself), and the decisions in force
by title and ADR-<n> label (read_record for the rationale and what the decision
turned down). Only an accepted
decision is listed: one still proposed, rejected, or superseded by a later
decision does not govern.
A decision that replaced another says which, on the same line as the rule: the line ends with
supersedes ADR-<n>: <title> (<state>), the label, the title and the state the replaced decision is in now
(the immediate one, never the chain behind it, and never its rationale). The opening’s JSON carries it as the rule’s
supersedes field, beside the id, as { found, id, adr, title, state }; found: false with only the id says the trees read
do not hold the replaced decision. read_record and show --json carry supersedes and supersededBy the same way
beside the raw ids of record, and the show verb prints each on one line, with the id after the name:
supersedes ADR-<n>: <title> (<state>) · <id>. The committed brief document carries only the trees that travel, so it
names a replaced decision only when the committed tree holds it.
The same field rides where the rules addressed at a file are told: each rule governing_rules lists carries supersedes
when it replaced one, and the rules pushed beside an edit end that rule’s line with supersedes ADR-<n>: <title> (<state>).
On those lines, and in the committed brief document, the title is cut at 120 characters and ends in an ellipsis when it is
longer, so a very long title of the replaced decision never costs the rule that replaced it its place in a text that is fitted
to a ceiling. A rule that replaced nothing is told exactly as before.
The fourth list is what is awaiting a judgement: a task IN_REVIEW, a decision
still proposed, a pattern proposed or reviewed: everything a person has to rule
on before it means anything. One list holds all three kinds, and each item says which
it is (kind) and what is owed (state). Each has a read that serves the rest by the
same id: read_record for a decision’s argument, skills for a pattern’s text,
next_actions for the verdicts a task in review allows. A pattern
awaiting a ruling is served only when the id is named (never in the list you get
by asking for none) and it arrives labelled with its state, because nothing can be
ruled on without being read. You read it with mnema show <id> instead, which serves
any state and records nothing.
Both lists ask what a state means (the position’s disposition, derived from the
machine’s own transition table) and neither asks whether a move is legal. That rule
would leave an accepted decision, which can always be superseded, pending forever; it
did exactly that to every completed task, which stayed on the work list because reopen
is always legal from DONE. So a task waiting on a reviewer is on the fourth list and
not the first, and a task that is finished or cancelled is on neither.
Three of the four lists are cut to the freshest items, and
workTotal/decisionsTotal/awaitingJudgementTotal say how many there were, so a
cut never reads as “this is everything”. The adopted patterns are the exception:
every one is listed, so that list carries no total. One more cut is by age and not by
count: a task blocked by the sweep of a session that stopped with no end (see What a session
handed to other agents) and last seen more than 14 days before the read is left out of the
work list and of workTotal, and workSweptLeftOut (absent when none) counts them,
which mnema status prints as one line. Nothing is written for them, they stay in the
record, and show, read_record and search serve them whole. A task a person blocked
is never cut this way, and neither is one whose block the sweep did not write. The mark
is the sentence of the reason, not who wrote it: a person who blocks a task by copying that
sentence sees it leave the opening after 14 days, still counted in the line and still found
by search and show.
The skills tool is what makes a recorded pattern usable: bootstrap lists the
patterns by name, and the tool hands over the pattern itself: the adopted ones as a
set, or one by id. It is the one read that also writes: consulting a pattern is
recorded against the session, once per skill, so the record can later show which
work was informed by which pattern. It records that the pattern was read, never
that it was followed; nothing observable here can tell those apart.
All of them, or all of their names. Asking with no id used to hand over every adopted body however many there were: over a record of 40 patterns of the median size that is one reply of 146,431 bytes, and 40 consultations recorded for a reader that had asked for none of them. So the bodies are weighed against what one read costs (20 KiB, the budget the published skills specification puts on activating a single pattern), and over that line the reply carries the names, says that it did and how many there are, and records nothing: a name is not a pattern, and nobody consulted text they were not handed. The body of the one that matters is still one call by id away, served whole and with no ceiling.
What arrives by default is what governs. Asking with no id returns the adopted
patterns and nothing else, which is the one thing the design protects: a candidate
handed over unasked would be a candidate read as an instruction. Asking by id also
reaches a pattern still proposed or reviewed, labelled with that state: a
rejected or deprecated one is refused, because a way of working the project
retired is worse to hand over than nothing.
A pattern’s body is the one thing mnema hands back as an instruction, so it does not arrive bare: alongside the bodies the reply states that this is content from your record, written by the people and agents working on it, and names the agent that adopted each one, or says a person did, or says that nothing has adopted it and therefore that it is not how the work is done here. It used to add that the text is “not instructions from mnema”, and no longer does: that is the idiom that marks content a model must not act on, in front of the one text served to be worked by.
# The terminal side of the same name: not the patterns, but where they came from.mnema skill provenance#> 2 pattern(s):#> 019faa06-30e1-… adopted public stacked-prs · proposed by claude-code · adopted by claude-code (the same agent)#> 019faa06-335f-… adopted public trunk-based · proposed by a person · adopted by a personAdopting a pattern is not restricted: an agent can propose one and adopt it, the same way it can submit a task and approve it. What this shows you is who did, which is the part that used to be invisible to everyone downstream.
Sending a pattern where a host will read it
Section titled “Sending a pattern where a host will read it”An adopted pattern can also leave the record as the SKILL.md an agent host reads
on its own: the published skills format, which routes on a description with
nobody calling anything:
mnema skill export 019faa06-30e1-… --out ./skills#> Exported skill "stacked-prs" to skills/stacked-prs/SKILL.md#> description: derived here from the body: its first sentence (or its first paragraph), collapsed to one line and cut to 1024 characters#> provenance in the file: record 019faa06-30e1-…, adopted by mnid:0872e852…, both checkable against this repository#> The record holds no description: this one was produced for the file and not stored. …The body goes out verbatim, the description is derived (or --description)
and never generated by a model, only an adopted pattern is exported, and the
metadata carries the id and the identity that adopted it so the file points back
at the record it came from. A derived description with no “when”, “whenever” or “quando” in
it is exported all the same, and the second stream says the host has no trigger to route by:
--description "Use when …" writes it again. It writes nowhere but --out and records nothing,
and there is no import in the other direction, on purpose: see the table above.
Asking somebody outside to date the record
Section titled “Asking somebody outside to date the record”The two local layers catch an edit that breaks the hash chain and an edit made without the signing key. Neither catches the person who holds the key, rebuilds the whole record from nothing and re-signs it: everything links, everything verifies, and the chain claims a history it acquired this morning. What that person cannot produce is an attestation dated before they started.
mnema witness stamp#> 8f21ab…-3c9d0e… (public): stamped: asked an outside witness to date checkpoint f84396…#> external witness (T3): pending: an attestation was requested from#> https://alice.btc.calendar.opentimestamps.org and has not confirmedWhat leaves this machine is the SHA-256 of one checkpoint’s signed message and
nothing else (no id, no title, no body, no count) and each calendar is handed a
hash of that digest with a nonce of its own, so no two of them see the same value.
One checkpoint is enough for the whole record: checkpoints chain, so an attestation
over the last one dates every checkpoint below it. Stamps are never removed and the
reading asks the newest one there is, so a record stamped in March and written to in
April still reports March: with the count of events written since, because a date
that does not say what it reaches claims more than it holds. A request that has not
confirmed yet survives the same way: verify names it and names the calendar it is
with, whichever checkpoint it was filed under, so nobody asked and somebody asked
and we are waiting are never the same sentence. The window is ordinary (the two
attestations in this repository’s own fixture were asked for at 00:52 and served
complete at 12:49), and the act that follows from reading silence is to stamp again.
The answer is a promise, not a proof. It becomes one when a Bitcoin block carries
it, minutes to hours later, and until then verify says PENDING, which is not
coverage. mnema witness upgrade is the return visit: safe to repeat, and it fetches
the 80-byte block header the proof lands in so that every verification after it is
arithmetic on this machine.
It goes back for every request the tail still has open, not only the newest one, and
that is not a refinement: a proof stays under the checkpoint it was filed under while the
head moves on every write, so by the time a calendar has an answer the request is usually
somewhere else entirely. Asking only about the head meant this verb answered
nothing has been asked about this checkpoint yet about records verify was reporting a
request in flight for: the same product contradicting itself about the same disk. It
walks newest first and stops at the first attestation that confirms, because a proof below
one that confirmed dates less of the record at a later instant, and no reading would ever
quote it:
mnema witness upgrade#> 8f21ab…-3c9d0e… (public): completed: the attestation over checkpoint f84396… has confirmed#> external witness (T3): covered: Bitcoin block 963688 at 2026-08-23T06:03:01.000Z
mnema verify --require=witnessed#> public: local integrity verified (T1/T2/T4) and witnessed (T3); 1 tail(s); all events are#> signature-covered; external witness (T3): covered, Bitcoin block 963688 at 2026-08-23T06:03:01.000Z#> (the work of its block header was checked here, not its place in the Bitcoin chain)What covered checked offline is said in the verdict itself: the block header did
the work it declares and the proof folds to it. Whether that header is in the Bitcoin
chain is not asked here (that needs a node or an explorer), so --require=witnessed
passing means the attestation is arithmetic that closes, not that a block was looked up.
A checkpoint normally has several confirmed attestations: three calendars land in
three blocks, and this record’s proof reaches 963688, 963689 and 963690. The one a verdict
quotes is the lowest block, which is the rule chain/FORMAT.md §8 names. It used to be
the first attestation a walk of the proof reached that had a header, which meant the date
this product published depended on the order a third party serialized an .ots file in,
and over this very record the reference verifier in chain/verifier/ read 06:03:01 while
the product read 06:23:18, twenty minutes apart from the same bytes. By height and not
by instant: a block’s nTime is declared by whoever mined it and consensus only bounds it,
so a lower block may carry a later one.
The address a return visit goes back to is checked before it is asked. Every open request names the calendar that took it, and that name was written by whoever answered: into a file a project can be handed. So the verb goes only to an https address at a public timestamp operator, follows a redirect only while it stays on the same host, and reports anything else by name without contacting it, in a line of its own: was not asked by this machine, which is a different fact from did not answer and calls for a different response. By operator domain and not by exact host, so a proof taken by a calendar nobody has heard of yet still completes.
verify never touches a network to read the witness (the one flag that reaches one
is --against-github, and only when it is given). It reads the proof and the header out of the
record, checks that the proof commits to the checkpoint it proved, that the path folds
to the merkle root the header carries, and that the header did the work it declares.
The proof itself is OpenTimestamps’ own file, unaltered,
so a stranger with the ots client and no copy of mnema gets the same answer from the
same bytes.
It is opt-in, and nothing on the writing path asks anybody for anything. A record nobody stamps behaves byte for byte as it did before this existed, and a checkpoint can never fail because a calendar was unreachable. What the layer does not prove: that the block header sits on the longest chain (a reader who needs that follows the block id into any explorer) and it says nothing at all about deletion or about whose key really signed.
Layout on disk
Section titled “Layout on disk”<repo>/.mnema/ the project record: commit this, the team shares it .gitattributes `* -text`: a checkout never rewrites the signed lines' ends tails/<id>/witness/ external attestations over this tail's checkpoints (T3) locks/ gitignored: writers' locks, and the projection a read keeps<git common dir>/mnema/<encoded path>/private/ this machine, this project only: beside the repository's objects, shared by its worktrees, kept when one is removed; <repo>/.mnema/private/ (gitignored) outside a repository~/.mnema/global/ this machine, across every project~/.mnema/identity/ the signing key: referenced, never copied into a chainA read keeps what it built in locks/projection.db of each tree it read, so the next
read takes only what was appended since instead of building it again. It is derived and
never the record: deleting it costs the next read one rebuild (the one edit it can miss is an in-place edit, to the same size, of a
line before the last of a live segment, which only mnema verify sees), it
sits where the tree’s own .gitignore already looks away, and a clone never carries it.
A read of a tree nobody has written to creates nothing, and where the file cannot be
had (a read-only directory, a damaged file), the read builds it in memory and answers
the same.
The MCP server keeps it for the two trees of the project it is in, and for no other tree: the
other projects of the workspace and the global tree are built in memory for the connection, as
before. Two questions are put to every project of the workspace, though: where the agent left off
(its runs are in whichever project it wrote) and whether the count of calls is switched off. For a
tree the connection has not otherwise read, the server keeps the answer to both on this machine,
under cache/ of the mnema home (~/.mnema, or wherever MNEMA_HOME puts it), never inside a
project; the next session believes it only while the tree is as marked (the extent of its tails, the
fingerprint of its sealed segments, the last line of each tail and the stamp of the code), and otherwise
reads the tree and writes the file again. A file of that kind that arrives with a clone is never
read. So a tool that only reads (readOnlyHint: true) may create or update that one file in the
project it serves and, beside the local count of calls, that index in the machine’s data
directory, and writes nothing into any other project; a read still appends nothing to the record and
rewrites no byte of it. Deleting the index costs the next read the time it saves.
Whatever moved the record behind the file (a command in another terminal, a git pull, a
checkout of another commit) is read by the next call, and a file that does not follow the
record is built again. The line a stored position ends on is read back and has to be the entry it ended on, so a
live segment replaced by another branch’s, longer or not, is read again, by the command line and
by a server that is already up; and a file another process or version of the code rewrote is taken
again before the next answer. What it does not see is an edit, in place and to the same size, to a
line before the last one of a live segment (mnema verify is what rules on that).
MNEMA_CACHE_DIR keeps that file somewhere else: set to an absolute directory that exists
and can be written to, every read (the command line, the MCP server and the library)
keeps its cache there instead of in the tree. It is for a checkout you cannot write to,
for several worktrees of one project that should build it once, and for CI, where the
directory is what the runner’s cache step saves and restores between runs, so a job starts
from the last run’s cache instead of rebuilding it:
- uses: actions/cache@v4 with: path: ${{ runner.temp }}/mnema-cache key: mnema-cache-${{ github.sha }} restore-keys: mnema-cache-- run: mkdir -p "$RUNNER_TEMP/mnema-cache" && mnema verify env: MNEMA_CACHE_DIR: ${{ runner.temp }}/mnema-cacheIt is still a cache and never the record. A directory may be shared by any number of projects: each file is named for the tree it was built from, so a tree reads only a file made from it, and a file that no longer follows the record, or that cannot be opened, is built again. A value that is relative, or a directory that does not exist or cannot be written to, is refused with what to do rather than ignored.
The SDK honors MNEMA_CACHE_DIR only when openRecord uses the default environment; with an explicit
env it is ignored, as MNEMA_HOME is. When a fresh install creates a tail, the project moves to another
cache file and the old one stays in the directory: deleting old cache files is safe (it is only cache), and
in a cached CI directory it means the directory can grow.
~/.mnema is this machine’s data directory, whatever $XDG_DATA_HOME says.
MNEMA_HOME moves it: set to an absolute path, the key root and the global tree live
directly under that directory; set to a relative one, every command refuses rather than
keep a key under whatever directory it was run from.
The project root is found by walking up from the working directory until a .mnema/
appears, the way git finds .git, so every command works from a subdirectory.
The walk never takes your home directory for a project, and stops there: ~/.mnema is
where the key lives, and a project in the home would own every folder under it.
mnema init refuses to found one there, and a .mnema/ that holds a key root is never
taken for a project anywhere. Only the committed tree is meant to be shared; the private
key never leaves the key root, which carries a .gitignore of its own: a home kept under
git never stages it, while the global tree beside it stays yours to version or not.
Why not $XDG_DATA_HOME? Because the key is who you are, and $XDG_DATA_HOME is set
on your behalf by whatever launched the process. A snap-packaged editor used to point it
inside its own revision folder for the processes it started, and Flatpak points it inside
each app’s sandbox; a key root that followed it made one person two identities, depending
on which program started the writer, silently and for good in every record it touched.
gpg and ssh keep their keys under the home for the same reason. The one variable that
moves the key is this product’s own, MNEMA_HOME, and nothing sets it for you.
What lives here
Section titled “What lives here”cli.ts: the binary (it checks the Node it runs on and only then loads the program, so a Node below the floor is told so instead of failing in an import).program.ts: the command surface (one verb per capability, argument parsing, and the printing). Every action resolves the trees, calls one function, prints.commands/: one module per verb, each a pure function from a context to a result, so a command can be tested without a terminal.mcp/: the agent surface (server.tswires tools onto the protocol,tools.tsholds the same adapters as thin functions, andsession.tsowns the run a tool call belongs to).
The domain is not here. To understand why a move is refused, read the workflow
gates in @mnema/core; to audit the proof itself, read @mnema/chain.
mnema 0.1.4 · commit cebc4e8da68c