Technical
Audit log format.
Every Loupe case carries an append-only, SHA-256 hash-chained audit log. Modifying any past entry breaks every subsequent hash — tampering is detectable without trusting the editor. The audit log ships in the export bundle, so recipients can verify the chain independently.
Entry shape
Each entry is a JSON object with five required fields. New event kinds may extend the data object without breaking older readers — unknown fields are ignored.
{
"seq": 42,
"kind": "source.attached",
"ts": "2026-04-27T14:02:41.108Z",
"data": { /* event-specific payload */ },
"prevHash": "8a4c…f12b",
"hash": "ef38…91d4"
}- seq
- integer
- Monotonic 1-indexed sequence number. seq=1 is always case.opened.
- kind
- string
- Event kind. See the table below for the seven kinds emitted in v1.
- ts
- ISO-8601 UTC
- Wall-clock timestamp at the moment Loupe wrote the entry.
- data
- object
- Event-specific payload. Contents documented per-kind below.
- prevHash
- hex SHA-256
- Hash of the previous entry. Empty string for seq=1.
- hash
- hex SHA-256
- Hash of this entry's canonical encoding (see Hash construction).
Event kinds
v1 emits seven event kinds, covering every operation that mutates case state. The set is closed — there is no “arbitrary user note” entry that could be used to inject text into the chain.
case.opened
Fires when: Case is created
Records: Case UUID, problem ID, operator-given name, ISO-8601 timestamp.
source.attached
Fires when: A log file is attached to the case
Records: File path, SHA-256, size in bytes, detected format + confidence, relationship classification (Trigger / Context / Correlated / Control / Unrelated), justification text if the relevance band was Weak or Unrelated.
source.detached
Fires when: A log file is removed from the case
Records: File SHA-256 (the same one recorded at attach time), and the operator-supplied reason for removal.
status.changed
Fires when: Case status moves between Open / Known Error / Closed
Records: From-status, to-status, ISO-8601 timestamp.
rca.section_edited
Fires when: Any section in the RCA editor is saved
Records: Section ID, template ID, byte length of the saved content, ISO-8601 timestamp. The content itself is not duplicated in the audit log — it lives in the encrypted case file.
narrator.run
Fires when: Narrator brief is generated
Records: Outcome (brief-ready / inconclusive / cancelled), wall-clock duration, internal consensus signals — none of which surface to the user-facing UI but are preserved here for forensic review.
export.completed
Fires when: Export bundle is written to disk
Records: Destination path, SHA-256 of every file in the bundle, redaction targets applied, raw-logs-included flag, encrypted-zip flag, ISO-8601 timestamp.
Hash construction
For each entry, the writer computes:
canonical = JSON.stringify({ seq, kind, ts, data, prevHash })
hash = sha256_hex(canonical)JSON encoding is canonical: keys in declaration order (seq, kind, ts, data, prevHash), UTF-8, no insignificant whitespace, no trailing newline. The hash field is not included in the input — that would be circular.
For the first entry, prevHash is the empty string "". Every subsequent entry chains forward.
Verifying the chain (recipient)
The audit log ships in the export bundle as part of the case metadata. To verify it independently, reconstruct each hash and check the chain:
# Pseudocode — your IR pipeline can use any language
for entry in audit_log:
expected_prev = "" if entry.seq == 1 else previous_entry.hash
assert entry.prevHash == expected_prev, "chain broken at seq " + entry.seq
canonical = json_canonical({
seq: entry.seq, kind: entry.kind, ts: entry.ts,
data: entry.data, prevHash: entry.prevHash
})
assert sha256_hex(canonical) == entry.hash, "hash mismatch at seq " + entry.seqA passing chain proves: (a) no entry was inserted, deleted, or modified after it was written, and (b) the entries appear in the order Loupe wrote them. It does not prove the timestamps are accurate to wall-clock time — for that, pair with a trusted time source (e.g. a signed receipt from RFC 3161).
What the audit log does not do
- No external timestamping in v1. Hashes are local. RFC 3161 timestamping authority integration is on the v1.x roadmap for cases that need legally-defensible wall-clock proof.
- No content duplication. The audit log records that an RCA section was edited and how many bytes; the content itself stays in the encrypted case file. Reading the audit log alone does not let an attacker reconstruct the writeup.
- No identity claims. Loupe is single-user; entries don't carry an actor field. For multi-actor environments (v2 candidate), entries will gain an actor identity bound to a Loupe-issued credential.