What it proves, and what it does not
Being exact about this is what the product is for, so it is the section worth
reading twice. mnema verify reads the events and the committed public keys of
a project’s trees (no private key, no network) and prints each verdict
verbatim. The surfaces never turn a verdict into a stronger claim than it is.
What holds. A hash chain over every entry, so a changed or reordered event
breaks it; Ed25519 checkpoints over a root recomputed from event content, so an
edit made without the signing key is caught, where the checkpoints remain, even if
the keyless hashes are recomputed (--require=signed fails when they were taken out); and a committed public key that verification re-derives from the key
material it loads, so swapping the committed key for another is caught too. The
verdict names the level it reached rather than saying yes or no.

One word of an accepted decision, changed by hand: verify fails and exits 1. A cut that takes the newest events together with their checkpoint would not be caught: a hash chain shows what changed, never what is gone. The script that made it is recordings/a-tamper-caught.sh.
What does not hold, stated as plainly as the rest.
| The claim somebody will read into it | What actually holds |
|---|---|
| Every event is signed | Every write the product makes leaves the record signed up to a checkpoint (every-write-signs-what-it-wrote.test.ts), but a reader is guaranteed only up to the last checkpoint: events written after it rest on the hash chain alone, and verify reports that separately instead of folding it into a pass. |
| Nothing was removed | Not proven locally, and it cannot be: a hash chain shows what changed, never what is gone, 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, exit 1. Committing the record to a git remote is what preserves the files a deletion would take. |
| The record is who it says it is | No. A record forged whole (deleted and refounded under a fresh key, with the opposite decision written into it) verifies clean and word for word like an honest one, and the second reader cannot tell them apart either. What would distinguish them is not in the record for any reader to find. Where somebody asked, verify --against-github compares the signing keys with the SSH keys the GitHub account each identity named publishes: that says an account publishes the key today, not that it was the account’s when it signed, and only on github.com’s word. |
| The record is as old as it says | Only where somebody asked for it. mnema witness stamp has the public OpenTimestamps calendars attest a checkpoint’s digest, so a chain rebuilt this morning cannot claim a history; what leaves the machine is a hash of that digest joined to a random nonce, and nothing else of the record; it is opt-in, and a record nobody stamped reads not covered. |
| A Sigstore signature names the person | It names an e-mail, not a GitHub login: whoever could sign in with that verified address countersigned the checkpoint (mnema witness sigstore). In GitHub Actions it names a workflow of a repository, not a person. It speaks for an identity of the record only where that identity named the same address in a signed, covered fact (mnema key sigstore), and verify --against-sigstore says whether it did. |
| A Sigstore bundle dates the checkpoint | On the word of Sigstore’s log: the time is Rekor’s clock, signed with Rekor’s key in a public log anybody can audit, not the Bitcoin work behind witness stamp, which is why it is no witness level and --require witnessed does not count it. It dates that checkpoint and every one below it; events written after are counted, not dated. |
| A Sigstore bundle names the agent, or the model, that worked in the run | No. From a GitHub Actions job the certificate also names the repository, the workflow, the commit and the run, and verify --against-sigstore prints those four from the extensions Fulcio wrote into a certificate Sigstore’s own checks accepted (packages/code/src/sigstore/sigstore.test.ts). It names a run, not the agent inside it: who started the run is not the agent that worked there, and nothing in the certificate says which agent or model that was. A certificate for an e-mail names no run. The second reader names each bundle and checks neither its certificate nor these four names. |
| Whoever holds the e-mail wrote the record | No. Anybody can countersign the digest of a checkpoint they can compute, which is any checkpoint of a public repository. Without the record’s own claim, a bundle dates the checkpoint and says nothing about who wrote it. |
| What leaves the machine, and when | For the stamp, a hash of a checkpoint’s digest joined to a random nonce, and nothing else of the record. With the Sigstore countersignature, where somebody ran it, the digest itself leaves, and the identity by design: the e-mail, or the repository, the workflow, the commit and the run, goes into a public log that does not forget, and into the committed bundle. In GitHub Actions the act refuses a private repository. |
| The record does not keep my e-mail | It keeps its hash: mnema key sigstore writes the SHA-256 of the address (sha256:<hex>, trimmed and in lower case), never the address. The hash protects the record and nothing else: the committed bundle and Sigstore’s public log name the address itself. And it is no secret from somebody who already suspects the address: they hash their guess and compare. |
| Checking a Sigstore bundle needs the network | Signing does; reading does not. verify --against-sigstore checks against the trust root this binary carries, so a bundle signed after Sigstore turns its keys over reads not covered on an older binary: never verified. The second reader names each bundle and does not check it. |
| A signed and countersigned record is honest | Whoever holds the key and the e-mail can rebuild the record and countersign it again; the date is then the date of the rebuild, which is what exposes it. Omission stays uncovered, as everywhere else. |
A green verify means the record is honest |
It means nothing verifiable is broken. The default rules on the hash chain, which a crude edit fails and a patient rebuild passes: --require=signed catches checkpoints taken out from under the events they signed, and it is one flag, not extra work; the second reader takes --require signed too, and has no --require witnessed. It does not catch a cut that took the newest events together with their checkpoint: what is left is a shorter record honest in every byte, and only a copy from before the cut, the history a git remote keeps, shows it was longer; verify --since <rev> holds the record to that copy, and the Action asks it of every pull request’s base. |
| The agent named on a fact did the work | No. Where the person’s key signs, which is the name the client announces: over MCP the name in its handshake (clientInfo), and where the client announced no usable one (it sent no initialize, as a revision of the protocol allows, or its name was blank) the default unknown-agent, with the run marked run.unannounced (FORMAT.md §6.9) so show, brief, the account, the record page and verify say no name announced in an MCP handshake beside it; a run written before 0.1.4 carries no mark, and its unknown-agent may be either, and a binary before 0.1.4 fails verify on a record that holds one (“Any version of the tool can read the record”, below); on the command line --which or the agent MNEMA_AGENT_KEY names, in the hooks the host’s name handed to the command. Where an agent key signs, the name is read from the record, which enrolled that key under it, and never from the client (the next row). The signature proves that the identity which signed stated it, not that it is true, and not that this agent, or any given model, did the work. Two things are enforced: an agent is never its own authority (a which equal to the who is refused, authority.test.ts), and the name is screened for secrets (every-door.test.ts). Underneath, under one operating-system user any process can read the key and announce any name: a which the signing key merely carries is DECLARED, and verify --verbose says so. |
| A fact signed by an agent key was signed by that agent | No. It was signed by a key a member enrolled for that agent (agent.enrolled, FORMAT.md §6.2), and verify counts it as ATTESTED in its census. Which process held the agent key, and which model drove it, is a property of the machine that the record does not carry. In full, below. |
| The gate protects what is recorded | It protects the shape of a change, not its contents, and it is not access control. Anyone who can run the command line 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. The workflows are part of the format (FORMAT.md §6.7): the gate judges a move by them before it writes, on the subject as the chain has it, and every reader judges each move again when it reads. A move written by hand with a valid key that the workflow does not allow (a rejected decision accepted, a state that does not exist) stays on the record, which is never edited, but no reader applies it, and verify names it in its census without failing: the move is intact and signed, and what it lacks is a place in the workflow. The second reader beside the format judges every move by the same tables and names the same moves, with the same rule (second-reader-agrees-on-what-is-not-applied.test.ts). Two machines that moved one decision apart without reading each other are a different case: both moves are applied; show says it, and brief says it of a rule it prints, one left in force (When two machines disagree). A binary from before the workflows were part of the format applies every move and names none. |
| The refusal of a write holds everywhere | Only in the editing tools of the hosts that refuse (the rung (c) and above of the table), alike in all of them. Where a rule of the record refuses a write at a path, the hook stops the host’s edit and write tools; it does not look at the shell, so sed -i or a redirect on the same file goes round it. The refusal says so, and so does the skill the agent reads; a hook on the shell is not there. Where a line of the record is one this version cannot read, as a newer mnema’s line is, the hook refuses every write it sees, naming the tree and the line, until mnema is updated or the line restored: in that project, or in every project of the machine when the line is in the global tree (the limit). |
| Only whoever wrote a note can take it back | Only for a binary from this version on. A retraction signed by another identity is refused when written, not applied when read, and named by verify in a census line, but it is a well-formed signed event, and an older binary reading the same record applies it and stops serving the note. The note is whoever wrote it FIRST: a second writing of its id by another identity is not applied either, and verify names it; an older binary serves the later writing, its content and its retraction. Where no citation links two writings of an id, the earlier at is taken, and a writing made by hand can be backdated to come first and take the note; the census names the other writing, so the contest shows. |
| A decision says what was accepted | Only for a binary from this version on. The text of a decision is the writing its first accept, rejection or supersede was made on: the first decision.recorded in the merged order among those that move’s writer had read, by the tails it cites (FORMAT.md §6.8). Any other writing of its id, signed by any key, another identity’s or its own, is not applied: every reading serves the text, the Action’s comment on a pull request included, and verify and the second reader name the other writing in a census line without failing. The product never writes one, since it mints a decision’s id when it records it. A writing dated back on a tail the accept never cited does not become the accepted text, whatever its clock says. Before any judgment, the first writing in the merged order is the text, and a writing made by hand can be backdated to come first; the census names the other one, so the contest shows, and whoever accepts is shown that text. A key that may judge the decision can judge it first, on a text it wrote, with a move dated before an accept that did not see it: that move binds the text, and the two moves are named as a divergence. An accept made by an older binary may have seen the LAST writing, which is what it served: if a writing dated back that it read precedes the honest one, this version serves the dated back one and names the honest one. An older binary serves the LAST writing, so a key of any identity can change the title and the rationale of an accepted decision there while it stays accepted, and its verify passes and names nothing. |
| A link taken back stops acting everywhere | Only where a binary from this version on reads the record. Taking a link back withdraws its own identity’s assertion of an edge, and every read that applies a link stops seeing the edge when nobody else asserts it (refs keeps it, marked retracted); the retraction follows the link to the first tree that holds the edge, so a copy of the edge another identity recorded in an earlier tree turns unlink into NOT_THE_AUTHOR with no way to point it at yours; a retraction by another identity is not applied and is named by verify. A binary from before link.retracted does not know the kind and stops reading the whole tree that holds one (the row below), so on that machine the rule neither acts nor stops acting until it is upgraded. |
| 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 (a call refused for arguments that do not match the tool’s input schema is not counted), 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 (the channels) 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. |
| Secrets stay out | Only the ones mnema recognizes by their format. A value in a known shape never reaches the chain; a proprietary token or a password written out in prose does, and nothing deletes a fact afterwards. It reduces the damage; it does not make the record safe to paste secrets into. |
| A key that signs only check results can do no harm | The role narrows what the key can sign, and only that. A leaked checker key can say that any rule’s check passed at any commit until somebody retires it (mnema key revoke --checker <fingerprint> --reason "<why>"); from the retirement on, verify refuses what it signs. What it signed before still verifies (a leaked key can date a result before its own retirement), so verify names those results in its census, and the record no longer vouches for them. Keep the key in a secret scoped to the job that runs the checks. |
| Any version of the tool can read the record | A newer one can, and from the first published version that is a promise: every version reads every record an earlier one wrote (FORMAT.md §7.1). An older one cannot: a binary from before a kind or a version of it existed stops reading the whole record once that line is in the committed tree, and, from the first published version on, refuses it by naming the kind and version, what it reads of that kind, and which mnema refuses: it cannot name the one that reads it, because nothing in the record says which version wrote a line. Read a record written by a newer version with a newer binary. Where no new kind or version is involved, an older binary reads the record under its own rules and refuses nothing: a rule is never loosened inside a kind and version that exist, but one tightened since (the workflows, the retraction by its author) is not applied there. |
The pattern underneath all of it: local cryptography covers alteration; the
history a git remote keeps covers omission and gives the signing key a history
someone else can check; mnema witness dates the record.
The reference carries the long form of this
table, claim by claim.
An agent key, in full
Section titled “An agent key, in full”A fact signed by an agent key was signed by a key a member enrolled for that agent (agent.enrolled, FORMAT.md §6.2); it shows the member’s own key did not sign it, and verify counts it as ATTESTED in its census. Which process held the agent key, and which model drove it, is a property of the machine (its sandbox, its users) that the record does not carry.
Every reader of this version, the second reader too, refuses a fact the agent key signs for another identity, under another name than the one it was enrolled as, or that changes who may sign (second-reader-agrees-on-enrolment.test.ts).
Under one operating-system user the agent key keeps the member’s own key out of the agent’s reach only while that key is protected by a passphrase no host is handed. mnema key request --agent <name> --anchor <identity> makes the key, in a folder of its own and in the clear, and mnema key enroll --agent <the line> records it with the member’s own key (whose passphrase, where it has one, is asked at the terminal, which is a door the agent’s host does not have), in the project’s committed tree and, where the key lives on that machine, in the project’s private tree and the machine’s global one (in the private tree and the global one, where the tree speaks for another identity than the line’s, the key’s consent is made again under that tree’s identity; the committed tree takes the line’s identity or refuses); a server or a hook started with --agent-key <name>, or a command that writes a fact an agent’s key may sign (every writing verb but init, key … and check run), with MNEMA_AGENT_KEY, then signs with it in each tree whose record enrols it under that name, and the which of what it records is the name the record of that tree gave the key, whatever the client announced. In a tree whose record does not enrol it (a project the verb was not run in, a tree made since), the member’s own key signs, and the agent the fact names is declared; and 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, naming the verb, and nothing is signed (in the hook what is not done is the recording of the fact, and the reply to the host is unchanged and says why). mnema doctor says which, tree by tree. The role belongs to the record that enrols the key, not to the key: a record that has not heard of that enrolment can enrol the same key as an ordinary member, vouched for by one of its own members, and verify accepts it there (FORMAT.md); inside one record a key.enrolled naming a key that record holds as an agent’s is refused by both readers. mnema key request --anchor <id> --key <file>, mnema key enroll <line> and mnema key enroll --checker <line> refuse (AN_AGENT_KEY) a key this machine keeps for an agent, in use or retired, or one a record they reach enrols as an agent, told by its fingerprint wherever its file is kept: a guard against a mistake on that machine, which a copy of the key on another machine does not meet. A retirement is a fact of one record too: key revoke --agent writes it in every tree of this machine that enrolled the key, another machine’s private and global trees keep counting the key until it is retired there, and mnema verify on that machine, once the committed tree holding the retirement is pulled, refuses in the trees it verifies (the private one, and the global one with --global) what the key signed there after it, and does not count it as attested. The second reader and the page read one tree at a time and do not make that cross. A retirement held in a tree not on the machine that verifies is not seen, and mnema verify does not say so. The cross has two limits of its own: 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 it compares the at each event carries, which the key that signs it writes, so a leaked key that writes an at earlier than the retirement passes it.
In CI the agent key is a secret a workflow hands to a job: a fact it signs shows that a job given that secret signed it (any step of that job, and whoever can run the workflow where the secret is given), and mnema witness sigstore names the run that countersigned the checkpoint, not the agent (the recipe).
mnema 0.1.4 · commit cebc4e8da68c