Audit log
Every prompt, response, tool call, tool result, permission decision, and hook firing lands in a local hash-chained audit log before it's shown in the UI. The log is the load-bearing artifact behind Kenaz's "you can prove what happened" pitch.
Where it lives
macOS: ~/Library/Application Support/Kenaz/audit/
Windows: %APPDATA%\Kenaz\audit\
Linux: $XDG_DATA_HOME/kaneaz-harness/audit/
The log is a SQLite database (audit.db) with WAL mode for safe concurrent writes. It's plain SQLite — you can query it with the sqlite3 CLI directly if you want to skip the UI.
What gets logged
Each event has:
id— a ULID; sortable by time.session_id— the session it belongs to (or empty for global events).occurred_at— RFC 3339 timestamp.kind— see the kinds table below.subject— the principal — usuallyuseror the model's provider+model identifier.payload— kind-specific structured data.prev_hash— SHA-256 of the previous event in the chain.hash— SHA-256 of(prev_hash || canonicalized payload || timestamps). Tampering with any past event invalidates every hash that follows.
Event kinds
| Kind | Fired when |
|---|---|
session.start | New session created |
session.end | Session deleted or finalized |
prompt.sent | A prompt was dispatched to a provider |
response.received | The provider returned a (full or streamed) response |
tool.invoked | The model requested a tool call |
tool.completed | The tool call returned (success or error) |
permission.requested | Kenaz prompted for permission |
permission.decided | User approved / denied / approved-for-session |
hook.fired | A hook ran (pre/post-send) |
artifact.saved | An artifact was persisted |
provider.added / provider.removed / provider.updated | Provider config changed |
corpus.built / corpus.queried | Corpus indexing or search |
memory.compacted / memory.edited | Memory compaction or manual edit |
settings.changed | Setting changed |
Browsing the log
Audit view in the left rail:
- Timeline — newest first. Filter by kind, session, time range, free-text search.
- Detail panel — click any event to inspect the full payload. Copies as JSON.
- Verify chain — runs through every hash from the genesis event to the latest. A failure points at the first invalid event so you know exactly where the chain was broken.
Verifying integrity
The hash chain is verifiable without the Kenaz UI. The verification logic is documented in audit/verify.md (in the harness repo) — short version:
prev = "GENESIS"
for each event in order:
expected = sha256(prev || canonical_json(payload) || occurred_at)
if expected != event.hash: TAMPER at event.id
prev = event.hash
The audit DB ships with the canonical-JSON serializer used at write time, so the verification is bit-identical.
Exports
Audit view → ⋯ → Export. Three formats:
- JSON Lines — one event per line. Best for piping into
jq,grep, or another tool. - CSV — flattened payload columns. Good for spreadsheets or quick visualization.
- Signed bundle — JSON Lines + a manifest with the hash range and a detached signature. The signature is over
sha256(jsonl_bytes || hash_range)and can be verified by anyone with the public key (Kenaz uses the user's local Ed25519 audit key, generated on first launch and stored in the OS keychain).
A signed bundle is the artifact you'd hand a compliance auditor or attach to an incident report.
Retention
Default retention is 365 days. Change via Settings → Logging → Audit retention:
7 days,30 days,90 days,365 days,forever.- Custom values via direct settings.json edit (
audit.retention_days: <N>).
A daily prune runs at 03:00 local time and removes events older than the cutoff. Pruning preserves chain integrity — the chain restarts from a "PRUNE" marker event with a fresh genesis hash so future verification still works.
If you want to archive before pruning, set up an audit.export_jsonl hook pointing at append-only storage; it'll write every event in real time and your audit DB can be pruned without losing history.
Privacy of the log itself
- The audit log is on-disk, on your machine, in a directory readable only by your user.
- It contains the exact prompts, responses, and tool arguments — including any sensitive content the model saw.
- Kenaz does not transmit the audit log anywhere by itself. If you want to ship it to your SIEM, set up a hook that exports each event as it lands.
- For tighter privacy, set audit retention to 7 days; the chain still verifies, but historical content drops off the prune cycle quickly.
Common queries
Open audit.db in sqlite3 (or use the Audit view → SQL scratchpad inside Kenaz):
-- Every shell command run in the last 24h
SELECT occurred_at, json_extract(payload, '$.command')
FROM events
WHERE kind = 'tool.invoked'
AND json_extract(payload, '$.tool') = 'bash'
AND occurred_at > datetime('now', '-1 day');
-- Permission denials in the last week
SELECT occurred_at, session_id, json_extract(payload, '$.tool'),
json_extract(payload, '$.reason')
FROM events
WHERE kind = 'permission.decided'
AND json_extract(payload, '$.decision') = 'deny'
AND occurred_at > datetime('now', '-7 days');
-- Provider spend by day (requires telemetry hook capturing tokens)
SELECT date(occurred_at) AS day,
json_extract(payload, '$.provider') AS provider,
SUM(json_extract(payload, '$.usage.total_tokens')) AS tokens
FROM events
WHERE kind = 'response.received'
GROUP BY 1, 2
ORDER BY 1 DESC;