A proof nobody else can check is not a proof.

A record is evidence only if someone who does not trust the recorder can check it. For that, they need a document that says what the bytes mean.

What the draft is

OATR, Open Agent Trace Records, is that document, in draft: the format Evidren already writes, an append only JSONL chain linked by sha256 and sealed at the tail by an HMAC. Sections marked FROZEN describe bytes that shipped before the draft existed.

What its status is

v0.1, draft, unpublished. No standards body has seen it and nobody has adopted it: its one implementation does not even use the name.

Independent work on agent audit trails exists, including an individual draft at the IETF. The plan is to converge with it rather than launch a competing name; whether OATR stays Evidren's format or joins that work is an open founder decision.

The recorder must not also be the referee.

A verification layer that keeps its format private asks you to take its word twice: once for what happened, once for whether the file says so.

Here the chain is sha256 over the raw bytes of each line. That is the entire algorithm, so any language with a hash function can verify it without Evidren.

line 1 of a real record, hashed by hand no Evidren in the loop
$ printf '%s' '{"v":1,"prevHash":"0000000000000000000000000000000000000000000000000000000000000000","ts":"2026-08-02T09:14:07.000Z","action":{"id":"a1","ts":"2026-08-02T09:14:07.000Z","source":"claude-code","sessionId":"demo","type":"shell","payload":{"command":"pnpm run build"},"context":{"cwd":"/repo"}},"decision":{"action":"allow","reason":"no matching rule","findings":[]}}' | shasum -a 256

99ff6d5dec1804bf0641088b0024e9bfd27f6eeb39b83c6e0e81a6440fbc1681  -

Fig. 1: that hex string is line 2's prevHash, verbatim. evidren verify does the same walk; it is a convenience, never the authority.

One record per line. Three fields that never move.

Records are newline delimited JSON, appended and never rewritten. Every record starts with these three fields.

v
Version of the format on disk. 1 for this draft.
prevHash
64 hex characters, the sha256 of the previous line's raw bytes.
ts
ISO 8601, stamped by the surface that saw the action. Never by the core.

A record is one of two kinds, told apart by which key is present. A decision record carries { v, prevHash, ts, action, decision, redactionRefs? }. An event record carries { v, prevHash, ts, event, detail }, for things that are not an agent action, such as a rejected repo config. A decision is one of four verdicts.

allow deny require_approval redact
the exhibit · raw bytes on disk one line, unwrapped
{"v":1,"prevHash":"99ff6d5dec1804bf0641088b0024e9bfd27f6eeb39b83c6e0e81a6440fbc1681","ts":"2026-08-02T09:14:07.000Z","action":{"id":"a1","ts":"2026-08-02T09:14:07.000Z","source":"claude-code","sessionId":"demo","type":"shell","payload":{"command":"rm -rf /"},"context":{"cwd":"/repo"}},"decision":{"action":"deny","reason":"denied by critical command-risk finding (rm-rf-root)","findings":[{"detector":"command-risk","code":"rm-rf-root","threat":"T1","severity":"critical","message":"recursive delete of the filesystem root","suggestedAction":"deny","evidence":{"safe":true,"category":"filesystem"}}]}}

Fig. 2: one decision record, exactly as it sits on disk.

Each line commits to the bytes of the line before it.

prevHash is the sha256 of the raw bytes of the record before it, without the trailing newline; the first record's is 64 ASCII zeros. Verification rehashes each raw line, compares it to the next line's prevHash, and reports the first mismatch by position, counting from one.

return value of verifyAuditChain, not CLI output broken
verifyAuditChain() → {"ok":false,"brokenAt":2,"reason":"prev-hash-mismatch"}

Fig. 3: one character of line 1 changed, pnpm run build to pnpm run xuild. The break is reported at line 2, the first link that no longer matches; a verifier cannot tell which of the two lines was touched.

An OATR record cannot be reformatted.

Nothing is reserialised, so there is no canonical JSON to agree on, and a pretty printed record is indistinguishable from a tampered one.

Every prefix of a valid chain is a valid chain.

So chaining alone cannot detect truncation: cut the last fifty records and the rest still links. The format seals the head to close that.

The seal is rewritten atomically on every append and compared in constant time. A missing, unparseable or forged seal on a record with lines is a loud failure, not a silent pass.

the seal, as the draft defines it §3 · frozen algorithm
chainKey   = HMAC-SHA256(key = redactionKey, msg = "signat-chain-head-v1")
chain-head = { seq, headHash, hmac }
               seq      : number of records in the chain
               headHash : sha256 of the last raw line
               hmac     : HMAC-SHA256(chainKey, seq + ":" + headHash)

Fig. 4: the derivation and its three fields.

the seal file from the same record head hash checkable with shasum
{"seq":2,"headHash":"cbff51f44fb6c440027a44cca7fdefeb61f031bfd49476b48f6680cbc14ebedc","hmac":"9798199d1d0f4b25fcee884c7bb4984d5c0554657597654a2185f24915aee35f"}

Fig. 5: that headHash is the sha256 of the raw record in Fig. 2.

the last line deleted, then verified again loud failure
verifyAuditChain() → {"ok":false,"reason":"chain-head-mismatch (tail truncated or replaced)"}

Fig. 6: truncation, caught and named.

Where the guarantee stops.

chainKey derives from the install's redaction key (§4), which never leaves the machine. A third party given the files can relink the chain and confirm the head hash, but cannot verify the seal, and every Evidence Pack says so in its manifest.

The chain does not prove the time either: ts is stamped by the surface that saw the action, so the chain establishes order, not clocks.

Closing the seal gap needs an anchor outside the machine. It is not built.

Evidence Pack manifest, quoted verbatim the product declaring its own limit
"externallyVerifiable": {
  "hashChainLinkage": true,
  "headHashMatchesLastLine": true,
  "d18HmacTailSeal": false,
  "d18Note": "HMAC seal requires the install redaction key, which is never exported"
}

Fig. 7: the limit, as a value in the exported file.

The field the draft held open has now been used.

shipped August 2, 2026

When the draft froze, deleting the record and its seal together left a home that verified clean, so the draft reserved a genesis head and marked the field v1 extensible. Since August 2, 2026, evidren init writes it, seq 0 under the install's key, so erasing both fails loudly; the draft's text still says reserved.

return values of verifyAuditChain, not CLI output erasure caught
# an initialized home: audit.jsonl and chain-head.json deleted, redaction.key left behind
verifyAuditChain() → {"ok":false,"reason":"chain-head-missing-on-initialized-home (audit erased; re-run `evidren init` to re-seal a genuinely empty home)"}

# redaction.key deleted as well
verifyAuditChain() → {"ok":true}

Fig. 8: run at e3a5122 on a freshly initialized home. The second verdict is the residual: an attacker who also deletes redaction.key leaves a home that looks never initialized.

The record proves a secret was masked without carrying it.

When a recognised secret is found, the payload is masked before it is written, and the record keeps a reference instead of the value (Fig. 9): an RFC 6901 JSON Pointer to the field and an HMAC-SHA256 digest of the secret, truncated to 16 hex characters.

The key is 32 random bytes in redaction.key in the Evidren home, written with mode 0600. It is created on first use and never leaves the machine.

the reference, as the draft defines it redact
redactionRef = { path, hmac }
                path : RFC 6901 JSON Pointer to the masked field
                hmac : HMAC-SHA256(per-install redaction key, secret), truncated

Fig. 9: a pointer and a keyed digest. Never the value.

a file write that carried an AWS access key masked on disk, not at display time
{"v":1,"prevHash":"0000000000000000000000000000000000000000000000000000000000000000","ts":"2026-08-02T09:14:07.000Z","action":{"id":"a2","ts":"2026-08-02T09:14:07.000Z","source":"claude-code","sessionId":"demo","type":"file_write","payload":{"path":".env","content":"AWS_ACCESS_KEY_ID=***REDACTED***\n"},"context":{"cwd":"/repo"}},"decision":{"action":"redact","reason":"sensitive content redacted","findings":[{"detector":"secrets","code":"aws-access-key-id","threat":"T3","severity":"high","message":"possible aws-access-key-id at payload /content","suggestedAction":"redact","evidence":{"safe":true,"redactions":[{"path":"/content","replacement":"AWS_ACCESS_KEY_ID=***REDACTED***\n"}]}}],"redactions":[{"path":"/content","replacement":"AWS_ACCESS_KEY_ID=***REDACTED***\n"}]},"redactionRefs":[{"path":"/content","hmac":"bd4f76b7f216c556"}]}

Fig. 10: the raw credential appears nowhere in these bytes.

Three properties follow.

Keyed, not a bare hash
A plain sha256 of a low entropy secret could be brute forced from the record.
Stable inside one install, meaningless outside it
The same secret always gives bd4f76b7f216c556 on this machine, so an auditor can match it across lines without holding it.
Independent of the verdict
A denied command that carried a recognised key is still stored masked.

Four defects, found by reading the draft against the code.

None is hard to fix; all four are why the draft is still a draft.

§0 overclaims
§0 promises a record an outside party can verify in full, but an outside party cannot check the seal (Fig. 7). §0 must be rewritten before publication.
§4 does not say how much is truncated
It calls the reference a truncated HMAC and stops. The implementation keeps 16 hex characters; a second implementer cannot derive that, so two emitters would disagree.
§1 does not close the vocabulary
It gives four action types and an ellipsis. The implementation defines nine: shell, file_write, file_read, package_install, mcp_tool_list, mcp_tool_call, mcp_tool_result, network, git.
§5 defines conformance circularly
It points at Evidren's own tests, which grew from 29 at the freeze to 34. Passing them shows agreement with this implementation; a real suite must derive from the document alone.

Where the bytes on this page came from.

The draft is signat/spec/OATR-v0.1-DRAFT.md: 130 lines, 7997 bytes, measured at build time, extracted on July 25, 2026 from freeze commit 8a4be64. The product is now at 09fdbca, and the three files the draft documents have changed since.

Verified on August 15, 2026 at e3a5122: every record, seal and verdict on this page was produced by driving the built artifacts in packages/core/dist, and a fresh home reproduces Fig. 2's bytes and Fig. 1's hash. Only the seal's hmac differs, because a fresh install derives a fresh key.