Your auditor asks for evidence of what your AI agents did last quarter. You hand them a CSV export of the audit log. They ask one question: “Can you prove this was not edited after the fact?” Most teams cannot. The log lives in a mutable database, exported by the same system that wrote it. The auditor has to take the entire trail on faith, which is precisely the property that makes it not evidence.
This post explains how the platform constructs audit evidence that holds up without faith. A previous post covered why per-agent identity and a tamper-evident ledger matter for Claude Code and MCP deployments. This one goes deeper into the three properties that make the evidence independently verifiable: hash-chaining, per-event cryptographic signing, and export in a machine-readable compliance format an auditor’s tooling already understands.
The hash chain: every event commits to all previous events
The ledger is append-only and hash-chained per tenant. Each event carries the SHA-256 hash of the previous event in its prev_hash field. The chain hash of event N is computed over a canonical binary preimage that includes: event N’s own fields (tenant, sequence number, timestamp, actor, action, target, metadata digest, payload hash) concatenated with the prev_hash from event N-1. At sequence 1, prev_hash is all zeros — the genesis anchor.
The critical property: modifying, inserting, or deleting any event in the middle of the chain changes its hash, which breaks the prev_hash link of the next event, which breaks the next, and so on to the tip. A single edit is detectable by walking the chain and recomputing each hash.
The preimage is a fixed, length-prefixed binary encoding with a versioned domain separator — not JSON. This is a deliberate choice against three attack surfaces that a JSON-based hash would leave open:
- Key ordering: JSON object key order is not guaranteed by most serializers. A different key order produces a different hash even for semantically identical data, which creates false chain breaks — or worse, lets an attacker reorder keys to forge a matching hash.
- Whitespace and number formatting:
{"seq": 1}and{"seq":1}and{"seq": 1.0}are semantically equivalent JSON but produce different SHA-256 digests. - Concatenation forgery: without length prefixes, two adjacent short fields can be merged to forge a third long field with the same hash. Length-prefixing each field with its byte count (4-byte big-endian) closes this.
The implementation in core/internal/store/canon/canon.go is the single source of truth. Both Append (writing) and Verify (reading back) call the same EventHash function. There is no second implementation to drift:
func EventHash(e Event) []byte {
var buf []byte
buf = lps(buf, domainEvent) // "olivares.audit.v1"
buf = lps(buf, e.TenantID)
var seq [8]byte
binary.BigEndian.PutUint64(seq[:], uint64(e.Seq))
buf = append(buf, seq[:]...)
buf = lps(buf, e.OccurredAt)
buf = lps(buf, e.Actor)
buf = lps(buf, e.ActorKind)
buf = lps(buf, e.Action)
buf = lps(buf, e.TargetKind)
buf = lps(buf, e.TargetID)
buf = append(buf, fixed(e.MetaDigest)...)
buf = append(buf, fixed(e.PayloadHash)...)
buf = append(buf, fixed(e.PrevHash)...)
sum := sha256.Sum256(buf)
return sum[:]
}
The domain separator "olivares.audit.v1" binds the hash to its purpose and version. A hash from a different domain (a checkpoint, a payload, a metadata digest) can never collide with an event hash, even if the raw bytes happened to match.
Ed25519 signing: every event is its own anchor
A hash chain proves internal consistency, but not authenticity. An attacker with raw database write access could recompute the entire chain from scratch with altered events and produce a valid chain — different from the original, but internally consistent. Hash chains detect tampering; they do not prove origin.
Per-event Ed25519 signatures close this gap. Every event appended to the ledger is signed at write time. The signature covers a domain-separated preimage of the tenant, sequence number, and the event’s chain hash:
domain ("olivares.audit.event.v1") || tenant || seq (8 bytes, big-endian) || hash
The signature is stored on the event but is excluded from the chain-hash preimage by design. This is not accidental: if the signature were included in the hash, signing an event would change the hash it is supposed to attest. The signature attests the hash without altering it.
An external verifier holding only the public key can confirm each event individually: recompute the chain hash from the event’s fields, reconstruct the preimage, and verify the Ed25519 signature. If any event was altered after signing, the signature check fails for that specific event — the verifier does not need to trust the system that produced the evidence.
The platform also supports key rotation. A chain whose signing key changed mid-life verifies end-to-end by pinning the current key plus the prior generations’ public keys. The verify function accepts a set of candidate keys and considers an event valid if any candidate verifies it.
On top of per-event signatures, periodic checkpoints notarize the chain tip under a separate signature domain (olivares.audit.checkpoint.v1). For organizations that need to defend against host-level compromise — not just database-level — checkpoints can be signed by an off-box KMS/HSM key (AWS KMS, GCP Cloud KMS, Azure Key Vault) where the private key never lives on the host. Per-event signatures handle the DB-only attacker; off-box checkpoints handle the host-compromise attacker. The two threat models are distinct; neither signature alone covers both.
The ledger contract: sealed in the same transaction
A common failure in audit systems is eventual consistency between the state mutation and the audit record. The state changes, the audit write is queued or batched, and if the audit write fails the state change has already committed. The result: unaudited mutations that exist in the system but not in the evidence.
The platform enforces a stronger contract. Both the state mutation and the ledger seal happen in the same database transaction. If the seal fails, the entire transition rolls back — the state mutation never commits. This is not best-effort; it is deny-closed.
The session runtime ledger illustrates this. When a Claude Code session transitions state (created, launched, stopping, stopped, failed), appendRunEvent records the transition in two places atomically:
- The global hash-chained audit ledger via
sc.Audit().Append— the tamper-evident chain anchored by aPayloadHash. - The per-session queryable ledger — an append-only projection linked to the global chain by
audit_seq.
Both writes happen inside the caller’s Mutate transaction. The code comment in runtime_ledger.go states the design intent directly: “the ledger is the system of record, so if the seal fails the whole transition rolls back — it is NOT best-effort.”
The PayloadHash itself commits only to canonical, non-sensitive transition facts — the run reference, sequence, event type, state transition, and timestamp. It never includes transcript content, prompts, environment values, or secrets. The ledger proves what happened; it does not store what was said.
The same pattern applies to workspace file mutations in workspace_ledger.go. A file write, mkdir, move, or delete is sealed before the filesystem operation runs. If the evidence cannot be appended, the mutation does not execute. The seal carries the operation type, path, and a SHA-256 of the written content — never the content bytes themselves.
OSCAL export: machine-readable evidence your auditor’s tooling ingests
A tamper-evident ledger is necessary but not sufficient for an auditor. If the evidence is in a proprietary format, the auditor still depends on your tooling to interpret it. OSCAL — the Open Security Controls Assessment Language, maintained by NIST — is the format that closes this gap.
The platform exports sealed evidence packages as an OSCAL bundle containing three models:
- Component definition: the control plane’s capabilities expressed as implemented requirements against a compliance framework (NIST SP 800-53, ISO 27001, EU AI Act, and others). Each implemented requirement carries the control ID, the capability keys that evidence it, and the real status as a custom property.
- Assessment results: per-control findings with an OSCAL-compliant status. Each finding’s target carries
satisfiedornot-satisfiedwith the precise product status preserved in the reason field. - Control mapping: a crosswalk from framework controls to the platform’s capability reference model, using the OSCAL 1.2.0 control mapping model. The relationship is always
intersects-with— the capabilities address part of a control. It never asserts conformance; that assertion lives only in assessment results, gated on live operational evidence.
The honesty constraint in the OSCAL export is worth stating explicitly. OSCAL’s finding status enum has exactly two values: satisfied and not-satisfied. There is no “partial” or “by design.” A control that is partially implemented, addressed by design, gapped, or unmapped maps to OSCAL not-satisfied, with the real product status riding in status.reason and a custom property under the platform’s own namespace (https://olivares.ai/ns/oscal). The export never launders a partially-met control into satisfied. Only controls backed by live operational evidence at seal time receive OSCAL satisfied.
Every OSCAL document carries ledger anchor properties: the manifest hash, the ledger sequence number at seal time, the ledger hash, and the integrity verification result. These are the bridge between the OSCAL document the auditor reads in their GRC tool and the underlying tamper-evident chain they can verify independently.
What offline verification means concretely
“Offline verification” is not a marketing phrase. It describes a specific technical procedure: the verifier takes the exported evidence, runs a verification tool on an air-gapped machine, and confirms the evidence’s integrity without any network access to the system that produced it.
The platform’s archival export writes a directory tree of JSONL segments (one line per event, canonical JSON) plus a manifest per segment. The manifest records the segment’s sequence range, event count, first and last chain hashes, a SHA-256 of the events file, and the previous segment’s last hash for cross-segment continuity.
The offline verifier (VerifyArchiveDir) then does the following, entirely in constant memory with no network calls:
- Load manifests and pair them with event files. A stray events file (no manifest) or a manifest whose events file is missing is a failure. The unit of evidence is the pair.
- Stream each event file line by line. For each event, re-derive the chain hash from the archived fields using the same
EventHashfunction the live system uses. Compare it against the stored hash. Checkprev_hashlinkage and sequence gap-freedom. - Verify canonicality. Re-marshal each parsed line and confirm it produces byte-identical output to the on-disk bytes. This prevents unknown-field smuggling or duplicate-key attacks that would pass a hash check but carry hidden data.
- Verify per-event Ed25519 signatures. For each non-checkpoint event, reconstruct the signature preimage and verify against the pinned public key(s).
- Verify checkpoint signatures. For each checkpoint event, verify the signature under the checkpoint domain against the pinned checkpoint key(s). If only event keys are pinned (no checkpoint key), a checkpoint event is marked “unverifiable” — deny-closed, not skippable.
- Check cross-segment continuity. Confirm that each segment’s first sequence follows the previous segment’s last sequence plus one, and that the previous segment’s last hash matches the current segment’s
prev_segment_last_hash. - Verify the events file digest. The SHA-256 of the events file computed during streaming must match the manifest’s
events_sha256.
The verifier reports the first inconsistency it finds, with the specific sequence number and a machine-readable reason: hash-mismatch, prev-mismatch, seq-gap, event-sig-invalid, event-sig-missing, checkpoint-sig-invalid, count-mismatch, events-sha256-mismatch, or segment-link-mismatch.
An honest limitation: the offline verifier attests exactly the range it checked and nothing outside it. A removed prefix or tail is undetectable offline — the directory does not say where the chain began or ended. The verify report includes a per-tenant Ranges field with a StartsMidChain flag so the auditor knows exactly what was attested. The in-chain signed checkpoints of the live system cover the tail; the offline export covers the archived range. Together they form the complete attestation.
| Layer | What it proves | What it does not prove |
|---|---|---|
| Hash chain | Internal consistency; any edit breaks the chain | Origin (who wrote the events) |
| Per-event Ed25519 | Origin; each event was signed by the key holder | Defence against host-level compromise |
| Off-box checkpoint | Host-compromise resistance (KMS/HSM key never on host) | Per-event granularity (covers checkpoints only) |
| OSCAL export | Machine-readable, framework-mapped compliance evidence | That every control is fully met (only live evidence counts) |
| Archive verify | Offline re-derivation of all of the above | Events before or after the exported range |
The code path: from state mutation to sealed evidence
The sequence from a Claude Code session state change to verifiable evidence touches three layers. In runtime_ledger.go, the appendRunEvent function constructs a PayloadHash by SHA-256 hashing the canonical, length-prefixed transition fields:
func runEventPayloadHash(runRef string, seq int64, event, from, to, detail, atTS string) [32]byte {
h := sha256.New()
for _, part := range []string{
runRef, strconv.FormatInt(seq, 10), event, from, to, detail, atTS,
} {
_, _ = h.Write([]byte(strconv.Itoa(len(part))))
_, _ = h.Write([]byte{':'})
_, _ = h.Write([]byte(part))
}
var sum [32]byte
copy(sum[:], h.Sum(nil))
return sum
}
That hash is then passed to sc.Audit().Append, which assigns the next per-tenant sequence number, links to the previous event’s hash, computes this event’s chain hash via EventHash, signs it with Ed25519, and inserts the sealed event — all within the caller’s transaction.
The result: by the time the transaction commits, the session’s state change and its tamper-evident audit record are either both persisted or both rolled back. There is no window where the state changed but the evidence did not.
Links
- Product: Audit — the audit trail and evidence export
- Product: Compliance — framework assessments and OSCAL
- Security model — the trust model underpinning the ledger