
halo-record v0.2.42
Tamper-evident audit trails for AI agents: hash-chained Runtime Records, dependency-free, verifiable by anyone.
halo-record
Tamper-evident audit trails for AI agents — hash-chained Runtime Records, rendered as a Runtime Report your customers can check themselves.
Every action your agent takes (tool calls, model calls, data access, approvals) becomes one Runtime Record in an append-only, hash-chained log; the Runtime Report is that chain rendered as a self-verifying HTML page. Any party holding a checkpoint of the chain can verify the records behind it were never altered, without trusting whoever produced them — that checkpoint is the load-bearing piece: the chain alone is tamper-evident against everyone except the party operating the recorder (LIMITS.md §1). When a customer's security team asks "what did your agent do with our data?", you hand them a link instead of a paragraph. Security reviews already ask AI questions next to the SOC 2 checklist — and increasingly those questions come from ISO 42001, the EU AI Act's record-keeping articles, and customers' own questionnaires. Today a written assurance still passes. The bet behind this project is that it won't for long.
Featured in Help Net Security (August 2026).
The record format is open and free to implement. This package is the reference implementation: recorder, verifier, witness client, and report server.
Using halo-record, or thinking about it? Tell me who you are and what for → Who's using halo-record?
Check it yourself
You are being asked to put a recorder inside your agent. You should not take that on faith:
- Zero runtime dependencies. Standard library only.
pip install halo-recordinstalls exactly one package. - No network calls, except three opt-in ones — anchoring to a witness (sends the subject id, a record count, and two chain fingerprints — the head and the chain root), reading a witness's checkpoints back (sends the subject id), and the RFC 3161 timestamp (sends only a checkpoint's state hash to a Timestamp Authority). All are off unless you invoke them; record contents never leave your infrastructure.
- Raw tool arguments are hashed, with a redacted summary alongside. Arguments are stored as a canonical hash plus a summary: the argument text with known secret and PII patterns masked, capped at 200 characters. A short input that matches no pattern appears in the summary in full; hash-only mode (
summaries=False) keeps no summary at all. Redaction is best-effort (regex over common secret and PII formats plus an entropy catch-all): treat it as defense-in-depth, not a guarantee. Outcome fields you supply beyondsummaryseal as given (LIMITS §13). - Small enough to audit. ~5,300 lines of Python (code lines, not counting blanks and comments). Read all of it in an afternoon.
- Apache-2.0.
- The paperwork is first-class. LIMITS.md (what the chain can't prove), PRIVACY.md (what records contain and what leaves your machine), RETENTION.md (operating under a retention policy), and REVIEWERS.md — the four-command independent check plus a citation format for review findings.
What each layer proves — the load-bearing distinction in this project (LIMITS.md §1): a chain you hold yourself proves records were not edited, relative to a head someone already holds; only checkpoints held outside the operator prove none were removed; and no hash proves every action was captured.
| Claim | Self-held chain | + External checkpoints | + Trusted capture |
|---|---|---|---|
| Detect edits to an established artifact | ✔ | ✔ | ✔ |
| Detect rewriting of committed history | — | ✔ | ✔ |
| Detect missing/late checkpoints | — | ✔ (agreed cadence) | ✔ |
| Prove every action was recorded | — | — | depends on capture boundary |
See one before you install: a sample Runtime Report — fictional data, real chain, and it re-verifies itself in your browser while you watch.
60-second demo
No agent required. With uv, nothing to install:
uvx --from halo-record halo demo --serve
or the classic way:
pip install halo-record
halo demo --serve
Either one scaffolds a fictional support-agent vendor with two customers, witnesses the chains (with a local witness file standing in for one outside the operator — see LIMITS.md §1), serves their gated Runtime Reports, and opens the operator console in your browser. Then try the tamper test: delete a line from one of the .jsonl files and reload. The report catches it.
Record your own agent
One line at the boundary:
from halo_record import trace
agent = trace(run_my_agent, profile="my-agent", log="audit.jsonl") # wraps your entrypoint; records the run boundary to ./audit.jsonl — add record_call() or a framework adapter at each tool boundary to capture individual calls
A from halo import ... convenience shim also ships — but the halo name on PyPI belongs to an unrelated terminal-spinner package, and if that package is installed it wins the import. halo_record is unambiguous, so the examples use it.
Without log=, records go to ~/.halo/my-agent.jsonl (one chain per agent). The wrapper seals the run boundary; the evidence lives in the per-call records. Capture those with a framework adapter (matrix below) — or explicitly, which also shows how delegation links:
from halo_record import Recorder, record_call
rec = Recorder("audit.jsonl")
with record_call(rec, "crm.lookup", {"account": "acct-9"}) as call: # one sealed record per tool call
call.result = crm.lookup("acct-9")
with record_call(rec, "payments.refund", {"amount": 120},
parent_id=rec.last_record_id()) as call: # child links to the action that spawned it
call.result = payments.refund(120)
Then render the report:
halo report audit.jsonl -o report.html # one chain -> self-verifying HTML
halo serve ./records --port 8721 # all tenants, gated per customer
The quickstart ends when you are looking at your own agent's Runtime Report in a browser. If you got a JSONL file and no report, something is wrong: open an issue.
The verification block
If a guardrail or policy layer checked the action, its verdict can ride on the record — an optional block recording what the gate decided, sealed into the hash chain like every other field:
from halo_record import build
build("tool_call", "security", tool="payments.refund",
verification={"status": "allowed", "verifier": "gate/1.2",
"policy_ref": "sha256:1f3a...",
"checked_at": "2026-08-01T12:00:00Z"})
One naming note: the package exports a record function (the decorator), which shadows the halo_record.record module on the package object. Import from the module path directly when you want its internals — from halo_record.record import build — rather than import halo_record.record as record.
which seals into the record as:
"verification": {"status": "allowed", "verifier": "gate/1.2", "policy_ref": "sha256:1f3a...", "checked_at": "2026-08-01T12:00:00Z"}
record_call(...) accepts the same verification= keyword. status is required within the block; verifier, policy_ref, and checked_at are optional. What each status means: