1. emem
  2. Verify
  3. Guard
for developerscheck a draft here, or run the server that enforces it

Verify · emem-guard

emem-guard

emem-guard checks every emem citation in a draft before it goes out, and answers allow or deny with a code and a fix.

more

Each citation is looked up in the signed record. It catches a number that disagrees with the fact it cites, a citation pinned to the wrong place, and, if you ask, a measurable claim that cites nothing.

call
POST /v1/guard/verdict
mcp
emem_guard_verdict
account
none
source
crates/emem-guard
01try it
route
POST /v1/guard/verdict
field
texts, an array
blocks
nothing; advisory
corpus
this responder's own

01Try it

Paste a sentence with a citation in it, or pick an example. The answer comes from emem.dev, which runs the same engine as a self-hosted node in advisory mode: it tells you what a guard would say and blocks nothing.

POST /v1/guard/verdict, live on this originlive
Press the button. The response comes from emem.dev, not from this page.

The same call from a shell

runs against emem.dev as printedsh
curl -sS -X POST https://emem.dev/v1/guard/verdict \
  -H 'content-type: application/json' \
  -d '{"texts":["Bengaluru air temperature is 35 degC (emem:fact:defi.zb493.zezo.zcb35:nflpddk7zsncywguwjzk5koksseqfyx4jnngkuryrnd4aykqlpfq)."]}'

Recorded on 2026-09-29. The fact reads 28.0 degC, so the draft is denied, and the receipt (left out here) signs the fact cids the responder read to decide:

response from emem.dev, trimmedjson
{
  "action": "deny",
  "code": "PROV_VALUE",
  "fix": "correct_value",
  "reason": "EMEM-GUARD DENY PROV_VALUE token=emem:fact:defi.zb493.zezo.zcb35:nflpddk7... fix=correct_value leaf=-",
  "citations_found": 1,
  "checked": 1,
  "citations_verified_here": 1,
  "citations": [{ "state": "verified", "token": "emem:fact:defi.zb493.zezo.zcb35:nflpddk7..." }],
  "advisory": true,
  "schema": "emem.guard.verdict.v1",
  "receipt": { "primitive": "emem.guard.verdict", "fact_cids": ["..."], ... }
}
FieldRead it as
action, code, fixthe decision, why, and what to do about it; code and fix are absent on an allow
citations_found, checkedhow many tokens were in the text, and how many were looked up. An allow with citations_found: 0 is a clean verdict over nothing
citations[].stateverified, byte_mismatch, or not_held_here for a token this responder does not hold
receiptthis responder's Ed25519 signature over the fact cids it read; check it at /verify or POST /v1/verify_receipt

Send texts, not text: a body with no texts field is read as empty and allowed. A chat transcript can go in messages instead. Other envelopes (MCP, OpenAI, CloudEvents, OPA) have their own routes, listed in section 05.

02what it checks
reason grammar
EMEM-GUARD DENY <CODE> token=<token|-> fix=<fix> leaf=<leaf|->
never
a denial on a citation this node does not hold

02What it checks

Rules run in order of how much they establish, and the first one that matches decides. Every deny names one code and one fix.

CodeFires whenFixDefault
PROV_SIGa cited fact's signature does not verifyrefresh_tokenon
PROV_BYTESthe cited fact exists, but under a different cell than the token namesremove_referenceon
PROV_VALUEthe number beside a citation is not the number inside itcorrect_valueon
POLICY_MODULEa detection module you loaded denied (section 08)set by the moduleno modules
PROV_DRIFTa cited reading has moved past its band's thresholdrefresh_tokenon
GEO_ZONEa cited cell is on the operator's restricted listcontact_adminoff; self-hosted only
CLAIM_UNGROUNDEDa measurable claim about a place cites nothing that resolvescite_observationoff

PROV_VALUE judges agreement at the precision the writer chose, so 889.6439208984375 written as 889.6 m passes and 5000 m does not.

more

It fires only when the node read a numeric value for the citation and the sentence carries exactly one citation, so it never has to guess which number goes with which token. A citation placed right after the sentence it supports also counts.

Two codes no resolver emits yet. PROV_SIG and PROV_DRIFT are in the contract and the rule engine, and their tests pass, but the resolvers that ship (none, a responder over HTTP, and the hosted one) look a fact up by its content address and never report a failed signature or a drift. A lookup by content address already refuses altered bytes, which is what PROV_BYTES and a not_held_here state report.

What it does not check

Relevance. A citation that resolves is not proof it is about what the sentence says.

more

On 2026-09-28 "Bengaluru rainfall is 28.0 mm" citing the temperature fact above was allowed: the value matched and nothing compared rainfall with weather.temperature_2m. Compare the resolved band and place with the sentence yourself.

Content safety, personal data, secrets. The engine is a chassis for those. A node you run can load modules that detect them; the hosted route loads none.

Tokens it cannot find. A citation this responder does not hold comes back not_held_here and is allowed. It cannot be told apart from one minted by another responder, and denying it would punish an agent for citing somebody else's node.

03hosted or yours
hosted
/v1/guard/capabilities
procedure
/v1/guard/selfhost

03Hosted or your own node

Use the hosted route to check your own drafts. Run a node when a guard has to sit in a request path and stop something.

emem.devA node you run
blocks anythingno, advisory: trueyes, or shadow mode to measure first
resolves citationsagainst its own store, directlyagainst a responder you name with --responder, or not at all
signed bythe emem.dev responder keya key the node generates on first run
evidencea read receipt over the fact cids it reada hash-chained verdict log, one leaf per verdict
claim gatingper call, claim_gating: truea node setting, --claim-gating
restricted zones, modulesnoyes
vendor hooksno: /verdict/anthropic-hook answers 501 requires_your_own_nodeyes
/log/*, /modules501 no_verdict_log_hereyes

The vendor-neutral routes answer on both with the same bodies, so a client written against your node can be pointed at emem.dev and back by changing the host.

04run it
procedure
crates/emem-guard/SKILL.md
written for
an agent with a shell
default bind
127.0.0.1:8080, or PORT
body limit
10 MB

04Run your own node

One binary, no config file. The first run writes a signing key and opens a log in the data directory.

terminal, your own nodesh
$ curl -s https://emem.dev/v1/guard/selfhost | jq -r .skill > SKILL.md
$ git clone https://github.com/Vortx-AI/emem && cd emem
$ cargo build --release -p emem-guard
$ cargo test --release -p emem-guard
$ ./target/release/emem-guard --responder https://emem.dev --data ./var/guard

Without --responder the node signs and logs every verdict and verifies no citation, and its banner says so:

banner, your own node, no respondersh
emem-guard listening on 127.0.0.1:8080
  signer   zwkuk6ahc6qseltbq7expaz2omd2vjkqjhv5criovu53sqy7qmwa
  log      ./var/guard/verdicts.jsonl (0 verdicts)
  resolve  null
           no responder configured: citations are not verified, only logged.
           point one with --responder http://127.0.0.1:5051 to check them.
  mode     enforce
  verify   emem-guard --audit --data ./var/guard

With a responder, each citation costs one GET /v1/facts/{cid}. All citations in a transcript share a 400 ms budget, at most 64 are looked up, and anything past either limit counts as unresolved, which is an allow, never a deny.

GET /health reports signer_b32, verdicts_logged, log_head, mode, resolver, require_signature, the four rule switches, and verifies_citations, so a load balancer and an auditor read the same answer to "does this node check anything".

Pass the conformance suite first

Unit tests prove the handlers, not the server you stood up behind a proxy.

more

--conformance runs twelve checks over the wire against a running node and exits non-zero on any failure: health honesty, verdicts for unknown events and malformed and empty bodies, a large body accepted, a complete descriptor, every open route answering, idempotent replay, a fetchable leaf that verifies, an answer inside 1000 ms, and 100 requests at a concurrency of 16.

terminal, your own nodesh
$ ./target/release/emem-guard --conformance https://guard.your-org.example

A checkpoint platform typically refuses private and loopback addresses and does not follow redirects, so put the node behind TLS on a routable host. Proxies in front of it need their body limit raised too: nginx defaults to 1 MB.

Failure semantics

For a checkpoint webhook, a non-200, a timeout or an unparseable body is a transport failure, not a deny.

more

Under fail-open the request goes through unchecked, and repeated failures can trip the caller's circuit breaker. So every input gets a 200 and a verdict, including an unknown event type, a malformed body and an empty one. The node answers within an 800 ms self-budget and deduplicates on the request id, so a retry gets the answer already recorded.

05checkpoints
verdict routes
9, 7 vendor-neutral
hosted here
the 7 vendor-neutral
listing
/.well-known/emem-guard.json

05Checkpoints

One engine behind nine request shapes. A test asserts that every route reaches the same verdict on the same evidence.

RouteCalled byOn emem.dev
POST /verdict, /verdict/nativeany agent: texts or messagesyes
POST /verdict/mcpan MCP host or proxy, gating a tool call or its resultyes
POST /verdict/openaian OpenAI-shaped chat or moderations bodyyes
POST /verdict/cloudeventCloudEvents 1.0 producers such as Knative, Dapr, Argo Eventsyes
POST /verdict/policyOPA-style {input} clients, Envoy external authorisationyes
POST /verdict/batchan offline scan, up to 64 transcripts per callyes
POST /verdict/anthropic-hookAnthropic Inference hooks, signed with your organisation's secretno, 501
POST /verdict/claude-codeClaude Code client-side hooksno, 501

POST /v1/guard/verdict?shape=mcp (or openai, cloudevent, policy) reads and answers the same envelopes on the one hosted route.

Gating an MCP tool call

Put a proxy between the host and its servers and ask before dispatching.

POST /verdict/mcp, your own nodejson
{"jsonrpc":"2.0","id":7,"method":"tools/call",
 "params":{"name":"read_file","arguments":{"path":"notes.md"}}}

{ "allow": true, "id": 7,
  "verdict": { "action": "allow", "checked": 0 } }

On a deny the response carries result, already shaped as a CallToolResult with isError set.

more

Return it in place of running the tool and the model reads the denial and the fix where it reads tool errors. Send params.result instead of params.arguments to check a tool's output before the model reasons on it.

06claim gating
rule
claim_gating
default
off
measured
3 firings in 8,739 sentences of this repo's docs, 2026-08-06

06Claim gating

The other rules deny when a check fails. This one denies when no check was offered: a sentence states a measurable quantity about a place or time and cites nothing that resolves.

Because it denies on absence, it ships off.

more

Turn it on for one hosted call with claim_gating: true, as the uncited-claim example in section 01 does. On your own node, run it in shadow first: every rule runs, is signed and logged, and nothing is blocked.

terminal, your own nodesh
$ ./target/release/emem-guard --claim-gating --shadow --data ./var/guard
$ ./target/release/emem-guard --report --data ./var/guard
verdicts           5
blocked            0
would have blocked 1
fired rate         0.2000
citations checked  2
...
by code            enforced / observed
  CLAIM_UNGROUNDED          0 / 1

would have blocked is counted from the same append-only file an auditor reads, and GET /log/report serves the same counts. When the rate is acceptable, drop --shadow. The evaluation and the signed record stay the same; only the answer to the caller changes.

What counts as a claim

A sentence fires only if its unit is in a table where every row names the band that reports it: metres from copdem30m.elevation_mean, mm/day from weather.precipitation_mm, µg/m³ from cams.pm25.

more

The verdict names that band as claim.source_band. Bytes, seconds, hertz and currency have no band, so "the call took 800 ms" never fires.

Three more conditions must hold. The sentence has a world anchor (an ISO date, a year, a coordinate pair, or a preposition before a capitalised name). It is an assertion, not a question, hedge or conditional. It is prose, not code or configuration.

A citation that resolves clears the rule, and then the value rule applies to it. The uncited Sumatra sentence above, given Lusail's elevation fact as its citation, is no longer CLAIM_UNGROUNDED; it is PROV_VALUE, because that fact does not say 31 m.

07verdict log
per verdict
Ed25519 signature
per file
BLAKE3 hash chain
domain
emem.guard.verdict.v2
budget
800 ms per verdict

07The verdict log

A node you run signs every verdict and appends it to its log before the caller gets an answer. A denial names the leaf that records it.

scanfind emem: tokensstring work resolveup to 64 tokens≤ 400 ms shared modulesplug-in detectorsif loaded evaluatefirst rule to matchshadow applied signEd25519v2 preimage appendhash chainjsonl on disk answerallow or deny+ leaf 800 ms self-budget for the whole exchange, under a 1000 ms floor a caller can configure
Figure 1. One verdict on a node you run. The log is written before the answer, so no verdict can be acted on without a record. The hosted route runs the first four steps and signs a read receipt instead of appending to a log.
GET /log/entry/leaf_0, your own nodejson
{
  "seq": 0,
  "record": {
    "checkpoint": "emem.native.v1",
    "request_id": "01KZAWCH25KXHYHAJHWK03151D",
    "outcome": "allow",
    "evaluated": "allow",
    "mode": "enforce",
    "checked": 1,
    "decided_at_unix_s": 1785997968
  },
  "signature_b32": "p4cfqpvlyymax6wg5lrshfn2t7tt6stw73saprg3oi57pvdbl7gri...",
  "signer_b32": "zwkuk6ahc6qseltbq7expaz2omd2vjkqjhv5criovu53sqy7qmwa",
  "chain": "7a99eac5a05dac1a8b0e16ee9867d1daa03ac3aa55ff72ab77d10086abfd816f"
}

outcome is what the caller was told and evaluated is what the rules reached. Both are signed, so an operator cannot later relabel a shadow verdict as enforced.

Audit the chain

Signatures prove each entry; the chain proves none was removed.

more

emem-guard --audit prints the entry count and any bad signature, broken link or unparseable line, and exits 1 if the log is not intact, so a scheduled audit fails its pipeline. Edit one verdict from allow to deny and it reports that entry under both bad signature and broken chain. Delete a line and every remaining signature still verifies; the chain breaks at the gap.

terminal, your own nodesh
$ emem-guard --audit --data ./var/guard
entries        5
bad signature  []
broken chain   []
unparseable    []
intact         true

GET /log/head publishes the head for a witness to pin, and GET /log/entries?start=0 lets anyone mirror the log.

08modules
flag
--module NAME
state
GET /modules
hosted
none, by design

08Detection modules

Grounding is built in. Other detection plugs in, and a module's verdict is signed and logged like a native one.

Load withWhat it is
--module secret-patternsbuilt in: fixed-prefix credential shapes such as AKIA and ghp_; denies with POLICY_MODULE, fix redact_and_retry
--module webhook:URLyour own classifier over HTTP, 200 ms timeout; abstains when it times out
--module sidecar:PATHone line of JSON each way over a unix socket, for a closed engine that will not link against this binary
--signed-module FILE with --trust-publisher KEYa module whose manifest a publisher signed; nothing loads without a key the operator typed

A module declares a latency class and a data class.

more

A slow module never runs on the enforcing path, a fast one that misses its budget is demoted and stops being able to block, and an abstain is never a deny. --modules-no-text withholds transcript text from every module. The log records the module id, version and an evidence digest, never the matched content.

09decisions
status
open to challenge
where
/channel

09Design decisions

Choices that could have gone the other way.

DecisionReason
A token the node cannot find is never a denial.A node holds a subset of any corpus. From inside it, a token minted elsewhere and a forged one look the same, and a wrong deny is the worst failure a guard has.
Every uncertain case allows, including a bad signature on an inbound webhook.A non-200 is a webhook failure, which under fail-open sends the request through unchecked and can trip the caller's breaker. The node answers 200 with allow and logs the problem.
Claim gating ships off.It denies on absence. The detector, its conditions and the unit table are public, and shadow mode lets you measure it on your own traffic first.
The hosted route blocks nothing and loads no modules.A shared node that blocked other people's traffic, or ran somebody's classifier over their transcripts, would be a different product with a different trust story.

If one of these is wrong, say so on the channel. More diagrams of the guard are in the diagram gallery.