A log index proves what your pipeline chose to keep. Here is what a hash-chained, checkpointed, countersigned and timestamped stream adds — and where the guarantee honestly stops.
A coding agent ran a DELETE against a production database at 02:14. Three weeks later someone asks for the trace: an auditor, an insurer, a supervisory authority, sometimes opposing counsel. You open your log index, filter on the time window, and find a line. It says roughly the right thing.
The question is not whether that line is accurate — it probably is. The question is what it establishes to someone who has no reason to take your word for it.
What an application log establishes, and what it does not
A log index is a database: whoever operates it can write to it, rewrite it, and delete from it. That is not a misconfiguration, it is the point — you purge, you reindex, you apply retention. And nothing in the index distinguishes a line that was never written from a line that was erased.
Four properties are missing.
Integrity. Nothing binds a line to the ones before it. Change a field, remove an event, insert one after the fact: the result remains consistent with itself.
Order. Timestamps come from the host clock, which is settable, and several sources merged into one index reorder themselves according to ingestion latency.
Origin. A log line is not signed: anyone with write access to ingestion can produce a plausible one, indistinguishable from the rest.
Completeness. Sampling, batches dropped under backpressure, index rotation: nothing states how far the stream was supposed to go. A truncated trace looks exactly like a short one.
There is a calendar constraint on top of that. The AI Act requires deployers of high-risk systems to keep logs under their control for at least six months. Directive (EU) 2024/2853 on product liability, which applies to products placed on the market from 9 December 2026, covers software and provides for disclosure of evidence: a court can order production, and failure to produce can trigger a presumption of defectiveness. "Trust our index" is not an answer.
Chaining the records
The first building block is boring and effective: every record carries the hash of the previous one. record_hash is BLAKE3-256(JCS(record without record_hash)), and prev_hash repeats the record_hash of its predecessor. JCS is the JSON canonicalization of RFC 8785: two implementations, in two languages, produce exactly the same bytes to hash. The first record starts from a sentinel derived from the stream identifier, BLAKE3("SEALR-GENESIS" || stream_id), which rules out quietly re-rooting a stream.
{
"schema_version": "1",
"record_id": "01K2F8Q9V3M7XB4C6D0E2Y5H8T",
"stream_id": "01K2F8Q0R4S5T6U7V8W9X0Y1Z2",
"segment_id": "01K2F8Q1A2B3C4D5E6F7G8H9J0",
"seq": 41207,
"ts_wall": "2026-08-11T02:14:03.482Z",
"ts_mono_ns": 90184002391,
"record_type": "operation",
"subject": {
"agent_kind": "claude-code",
"agent_session": "01K2F8PZ0000000000000000AA",
"human_principal": "u_9134",
"attribution": "attributed",
"source": "mcp-proxy"
},
"action": {
"integration": "postgres",
"operation": "query",
"verb_class": "delete",
"target_resource": "db:prod-eu-orders",
"resource_labels": ["production", "pii"]
},
"payload_commitment": { "alg": "blake3-salted", "commitment": "7c1e93…" },
"verdict": {
"decision": "BLOCK",
"mode": "enforce",
"risk_class": "CRITICAL",
"policy_version": 17,
"rule_ids": ["block-unbounded-writes"],
"reason_codes": ["SQL_UNBOUNDED_WRITE"],
"guard": { "name": "sql", "version": "1.0.0" }
},
"prev_hash": "9f2c8e…",
"record_hash": "b41d07…"
}
Two details matter as much as the chaining. The action block is structural — verb class, target resource, labels — never the literal of the statement. The content appears only as a salted commitment, BLAKE3(salt || bytes), with the salt kept alongside the payload or discarded, never in the record. A redacted bundle therefore verifies in full, and whoever holds the payload and its salt can selectively prove that it matches that record.
The verifier also checks the sequence — strictly +1 within a segment — and the dual clocks: ts_mono_ns monotonic, ts_wall non-decreasing within ±2 s, a larger regression requiring an adjacent clock_anomaly record. Deleting a record in the middle produces a chain_link_broken or a seq_gap; altering one produces a record_hash_mismatch.
But chaining alone is not enough: whoever can rewrite the whole file can recompute every hash. It makes local edits detectable, not wholesale rewriting. For that, you need something from outside the file.
Checkpoints, Merkle roots, and countersignature
Signing every record would cost too much on the execution path. Integrity therefore rolls up to signed checkpoints: a record is attested as soon as it is covered by at least one valid checkpoint.
A checkpoint binds a Merkle root (RFC 6962) computed over the hashes of records [seq_from, seq_to], the current chain head, and the fingerprint of the Recorder's certificate. It is signed with Ed25519 by the Recorder identity key — held in a TPM 2.0 or a secure enclave where the platform allows it — and emitted at least every 1,000 records or every 10 seconds of activity. Checkpoints chain to each other too, through prev_checkpoint_hash: without that link, removing an interior checkpoint along with the records it attests would leave a self-consistent set.
Then comes the countersignature. The Console verifies the Recorder's signature, checks that seq_from equals last_seq_to + 1 against the head it has stored, and signs in turn; a divergence raises a chain_conflict that becomes evidence itself. This is the answer to a stolen Recorder key: a root-level attacker can sign a plausible parallel stream offline, but cannot get it countersigned or anchored retroactively.
{
"note": {
"checkpoint_id": "01K2F8QB7Y8Z9A0B1C2D3E4F5G",
"received_at": "2026-08-11T02:14:12.006Z",
"console_key_id": "console-eu-2026-03",
"stream_high_water_seq": 41333
},
"signature": "…"
}
Remember stream_high_water_seq: it is the most important piece in this article.
RFC 3161 and eIDAS qualified timestamps
A signature proves origin, not date. The Console therefore groups checkpoint hashes into batches of at most five minutes and submits the batch root to an RFC 3161 timestamping authority; the token is stored with the inclusion proofs. Each day, a consolidated root receives a qualified electronic timestamp from a qualified trust service provider, and that daily root is also published externally.
The value is legal as much as technical: under eIDAS, a qualified timestamp enjoys a presumption of the accuracy of its date and time and of the integrity of the data it binds. That is the difference between "our server said 02:14" and a date a third party can rely on.
Anchors accumulate, and adding more never invalidates a bundle. What time proves, however, is bounded, and the verifier says so in plain terms: a record is proven to exist no later than the time of its earliest anchor; the "no earlier than" side is bounded only by the previous anchor and monotonic clock deltas.
Verifying offline, without trusting the vendor
Evidence only the vendor can verify is not evidence. Exports therefore go into a self-contained container, the .seal bundle:
manifest.json # id, scope, tool versions, content hashes
records/<stream>.part-00000.ndjson.zst # zstd shards, in sequence order
records/index.json # shard listing and sequence bounds
checkpoints/*.json # checkpoint notes + countersignatures
anchors/rfc3161/*.tsr anchors/qts/*.der anchors/proofs.json
identity/certs.pem identity/revocations.json identity/roots.json
VERIFY.md # how to verify, pinned verifier versions
sealr-verify is published under Apache-2.0 and makes no network calls. It validates the certificate chain up to a trusted root, applies the revocation list, rechecks every hash and every link, recomputes every Merkle root, checks signatures and countersignatures, validates RFC 3161 tokens and qualified timestamps, then cross-checks coverage.
sealr-verify evidence-2026-08.seal \
--console-key console-eu-2026-03.pem \
--expect-high-water 01K2F8Q0R4S5T6U7V8W9X0Y1Z2=41333
The report comes out as text and as JSON, with an exit code you can use in CI: 0 pass, 10 pass with findings, 11 fail. The test suite requires that a single-byte mutation of a golden bundle produce a failure with the correct finding class.
The honest limit
This is the part most vendors skate past. We put it in the middle of the page: it is the only thing that makes the rest credible.
This evidence is about detection, not prevention. It establishes the integrity, ordering, timing, and origin of the recorded stream. It cannot prove that unrecorded events did not happen. An attacker with root on a host can suppress future recording; that is detected through gaps and reconciliation, it is not prevented.
Coverage is a deployment property, not a cryptographic one. A psql session opened directly, a git push from a machine outside the proxy: no hash chain says anything about them. What the product does is turn silent bypass into loud evidence — server-side GitHub App events are reconciled against recorded pushes, and a miss produces a coverage_gap record and an alert; Recorder starts and stops, policy changes, and clock anomalies are recorded and signed as well. We do not claim bypass prevention.
The end of a stream is the hard case. Cutting the tail off a hash chain — the records and the checkpoints attesting them — leaves a shorter chain that is perfectly self-consistent. No cryptography internal to the bundle settles it: a truncated stream and an honest shorter stream are indistinguishable from the inside. Hence two distinct verifier behaviors:
- with no external attestation bounding the stream's extent, it emits the
tail_completeness_unprovenwarning and states exactly what to do about it; - with an external high-water mark — the
stream_high_water_seqcarried by a retained countersignature, or the value passed via--expect-high-waterfromGET /v1/streams/{id}/integrity— missing trailing records becometruncation_detected, a hard failure. An attacker on the Recorder side cannot forge a lower value without the Console signing key.
That is the whole difference between an export that is self-consistent and one that is decidable. The verifier prints these limits — along with the fact that subject attribution quality is as recorded, not an independently proven property — in every report, including when everything passes.
What changes, property by property
| Property | Application log | Hash-chained evidence stream |
|---|---|---|
| After-the-fact edits | possible, invisible | detected (record_hash_mismatch) |
| Deletion in the middle | invisible | detected (chain_link_broken, seq_gap) |
| Deletion at the end | invisible | flagged by default; decidable with an external high-water mark |
| Order | host clock | strict sequence + dual clocks, deviations flagged |
| Origin | unsigned | Ed25519, hardware-held key, certificate chain, revocation |
| Defensible date | declarative | RFC 3161 + daily eIDAS qualified timestamp |
| Third-party verification | access to your platform | Apache-2.0 binary, offline, published format |
| Coverage | unstated | a deployment property, reconciled and reported |
To close
None of this makes an organization compliant, and none of it replaces access control. What it changes is narrower: the day someone asks what an agent did, you produce an artifact a third party can verify without you, and whose gaps are flagged rather than silent.
The format is published and the verifier is Apache-2.0. The right way to evaluate all of this is not to believe us, it is to try to make the verifier fail — on your own streams if you like, by requesting a demo.