Loupe
Documentation

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.seq

A 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.