Audit & tamper-evidence
Every decision Belay makes leaves a record, and those records are built to resist quiet tampering by a compromised agent. This page covers the hash-chained audit log, the evidence packs you can build and verify, the honeypot canaries, and — honestly — the limits of self-protection when running unprivileged.
When to read this
Read this when you care about proving what happened: for incident review, for CI evidence, or simply to understand why the audit log can't be silently edited out from under you. It builds on Verdicts — the decisions recorded here are the verdicts described there.
The hash-chained audit log
Belay appends every decision to a local audit log at ~/.belay/audit.ndjson
(one JSON row per line). Each row is hash-chained to the one before it:
hash = sha256(prev_hash + canonical_json(row))
canonical_json is a recursively key-sorted, whitespace-free encoding of the
row, so the hash is stable and reproducible. Because each row's hash folds in the
previous row's hash, the log behaves like a chain: you cannot alter, reorder, or
delete a row in the middle without breaking every hash that follows it. Tampering
becomes detectable, not invisible.
The audit log lives under the invoking user's home directory because Belay runs
as that user, never root. Its self-protection layers specifically treat this file
as a protected path (see below). On Windows the data directory is
%ProgramData%\Belay instead of ~/.belay.
You can inspect recent rows directly:
belay status # the most recent audit rows (last 20)
belay logs -n 100 # the last N rows
The audit-view commands are belay status and belay logs. There is no
belay audit command.
Evidence packs: build and verify
For a portable, checkable record — for example to attach scanner results to a CI run or hand off to a reviewer — Belay produces evidence packs.
belay evidence build # write a tamper-evident pack
belay evidence verify # recompute and check it
evidence build snapshots your last 500 audit-log rows into findings + a
manifest.json that lists a SHA-256 hash per file in the pack (the SARIF file it
writes today is a fixed empty stub, not real scanner output). evidence verify
recomputes those hashes and compares them against the manifest, so any post-hoc edit to
a file in the pack is caught. The pack is a self-contained, verifiable snapshot.
For CI code-scanning, feed the scanner's own SARIF 2.1.0 output (belay scan --format sarif) into your pipeline directly — the evidence build SARIF file is a placeholder,
not a substitute for it.
Honeypot canaries
Belay plants canary files — decoys that no legitimate workflow should ever
touch. There's a fake AWS credentials file (with a decoy access-key sentinel) and
an .env sentinel token (BELAY_CANARY_TOKEN). Any read or egress of their
sentinel bytes trips a Critical verdict immediately.
The idea is simple: a legitimate agent has no reason to open a fake credentials file, so a read of one is a high-confidence signal that something is enumerating or exfiltrating secrets. The canary turns a stealthy secret-hunting step into a loud, recorded event.
Self-protection layers
Belay guards its own rule catalog, audit log, and binary against a possibly-compromised agent using complementary layers:
- Command layer. On hook-visible tool calls, a direct write or edit to a
protected path (the rule catalog, the audit log, the
belaybinary) is denied. A Bash command that applies opaque external content (for examplegit apply,git am, orpatch) is surfaced for human review as an ask. - Kernel layer (optional, privileged, Linux). With the opt-in eBPF sensor
(
--features ebpf), a process that writes a protected file outside the hook path is caught via a kernel write event and the reflex kills the process. This is detection and reactive kill, not pre-write blocking. - Startup integrity check (unprivileged). The build embeds a SHA-256 of the rule catalog. At daemon startup, a drifted on-disk copy triggers a loud alert. The running binary's compiled-in rules are unaffected regardless.
Honest limit: no unprivileged pre-write blocking
Belay runs unprivileged by design. True syscall-level blocking — denying a
write in the kernel before it happens — requires CAP_SYS_ADMIN/root and is
intentionally out of scope.
What you get instead is a layered, mostly tamper-evident posture: writes through the hook path are blocked at the command layer; out-of-band writes are (on Linux, with the privileged sensor) detected and reacted to; and the audit log's hash chain plus the evidence manifests make after-the-fact tampering detectable. The design goal is that tampering cannot happen silently, not that an unprivileged process can veto every possible kernel write.
Related docs
- Verdicts: Allow / Ask / Deny — the decisions recorded in the audit log.
- How it works — the boundary and engine that produce those decisions.
- Installation — installing and running the daemon that writes the log.