Every gated action produces an Action Receipt whose action_hash is a BLAKE3 digest over its canonical bytes. The audit chain threads those per-receipt hashes together, so the whole log gets the property a single receipt has — you can prove it has not been edited, offline, without trusting HESO.
Why a chain
A standalone receipt proves the operator authorized one action under a known policy. It says nothing about its neighbors: on its own, a receipt cannot tell you whether an earlier action was quietly deleted, reordered, or rewritten after the fact. A chain closes that gap.
Each entry commits to the entry before it. To forge a clean-looking history you would have to recompute not just the receipt you touched but every link after it — and the result still would not match the hashes anyone else already holds. The chain turns a pile of independently-signed receipts into an ordered, append-only record.
- Ordering — the sequence number plus the back-reference fix where each action sits relative to the others.
- Completeness — a missing entry leaves a gap that the next link cannot bridge.
- Immutability — editing a past receipt invalidates every chain hash that followed it.
The link function
The chain is one recurrence. For a receipt at sequence number seq, the chain hash folds the previous chain hash together with this receipt’s action hash:
chainHash(seq) = BLAKE3(prevChainHash ++ actionHash)
The very first entry, seq 0, has no predecessor, so it uses the literal string "genesis" as its prevChainHash. From there each link feeds forward: the chain hash you compute for one entry becomes the prevChainHash input for the next. The Node core exposes the primitive directly as chainHashHex.
import { chainHashHex } from "@hesohq/core" // seq 0 — the genesis link has no predecessor receipt.const prev0 = "genesis"const chain0 = chainHashHex(prev0, actionHash0) // seq 1 — fold the previous chainHash into the next receipt's actionHash.const chain1 = chainHashHex(chain0, actionHash1) // chainHash(seq) = BLAKE3(prevChainHash ++ actionHash)
The same BLAKE3 implementation that hashes a receipt’s canonical bytes computes the chain link — it lives in the Rust core, and Python, Node, and the browser WASM all call it. A chain hash is byte-identical wherever it is derived, so there is no re-implementation to drift.
The audit entry
Each link in the chain is recorded as an AuditEntry. It carries the receipt’s action hash, both ends of the link, and a few fields copied from the receipt — the verb, decision path, and trust level — so you can scan and filter a log without re-parsing every receipt.
- seqnumberrequired
- Zero-based position in the chain. seq 0 is the genesis entry; each subsequent entry links back to seq − 1.
- runIdstringrequired
- Identifies the agent run the action belongs to, so a chain can group the actions of a single workflow.
- actionHashstringrequired
- The receipt’s
action_hash— a BLAKE3 lowercase 64-hex digest over its canonical bytes. This is the leaf the link commits to. - prevChainHashstringrequired
- The chain hash of the entry at
seq − 1, or the literal"genesis"forseq 0. - chainHashstringrequired
- This entry’s link:
BLAKE3(prevChainHash ++ actionHash). It becomes theprevChainHashof the next entry. - verbVerbrequired
- The action verb, copied from the receipt — one of
llm_call,tool_call,http_request,payment,data_export,account_change, ordelete. - decisionPathDecisionPathrequired
- The policy decision that gated the action:
allow,block,redact, orrequire_approval. - trustLevelTrustLevelrequired
- The receipt’s trust level:
L0(operator-signed) orL1(operator plus human approver co-signature). - clockstringrequired
- The capture timestamp carried forward from the receipt, in ISO 8601.
A single entry, for the vendor payment from the introduction:
{ "seq": 1, "runId": "vendor-payouts-2026-01-14", "actionHash": "9f2c…e1c0", "prevChainHash": "0a77…42bd", "chainHash": "b3e5…91da", "verb": "payment", "decisionPath": "require_approval", "trustLevel": "L1", "clock": "2026-01-14T18:04:31Z"}
Detecting tampering
The chain is tamper-evident because every field that gets hashed feeds the next link. Suppose someone edits an earlier receipt — say, lowering a payment amount after the fact. That edit changes the receipt’s canonical bytes, which changes its BLAKE3 actionHash. Because that action hash is an input to its own chainHash, the link no longer reconciles — and since that chain hash is the prevChainHash of the next entry, the break cascades forward through every entry to the end of the log.
Verification walks the chain from seq 0 and recomputes each link. The first sequence number where BLAKE3(prevChainHash ++ actionHash) does not equal the stored chainHash is the tampered entry. So you learn not just that the log changed, but exactly where.
The chain proves the log is internally consistent and unedited — that no past entry was altered, dropped, or reordered without leaving a visible break. It does not prove an entry was the only action that ever happened: a chain you hold can still be a truthful prefix of a longer one, or a fork. For an external guarantee that two parties saw the same history, anchor the log in a transparency log.
Verifying a chain
Verification is offline and deterministic. Hand the SDK an ordered array of entries and it re-derives every link, checking each chainHash against BLAKE3(prevChainHash ++ actionHash) and confirming that each entry’s prevChainHash matches the previous entry’s chainHash. On the Node core this is verifyChain; the same logic is exposed for audit logs as verifyAuditChain.
import { verifyChain } from "@hesohq/core" // Re-derives every link from seq 0 forward and checks each// chainHash against BLAKE3(prevChainHash ++ actionHash).const result = verifyChain(entries) if (!result.ok) { // result points at the first seq whose link no longer reconciles. console.error("chain broken at seq", result.brokenAt)}
Because the link function lives in the Rust core, the result is identical whether you run it on a server with @hesohq/core or in a reviewer’s browser. Chain verification checks the structure of the log; verifying each receipt’s signatures and hash is the separate seven-gate verify order. A trustworthy log needs both: every receipt valid, and every link reconciled.
Anchoring in a transparency log
A BLAKE3 chain you hold yourself is tamper-evident, but it is still your copy. To let an outside party confirm that a receipt is in the same history everyone else sees — and that the log has only ever grown, never been rewritten — anchor the chain in an RFC-6962 Merkle tree.
That gives you two cryptographic proofs over the receipt log: an inclusion proof that a specific receipt is in the tree, and a consistency proof that a later tree is an append-only extension of an earlier one. The chain links the entries to each other; the transparency log links your copy to a shared, public root.
