Logging and audit retention
Where grith writes its logs, what the audit database records, and how long any of it is kept.
grith writes two different things. The operational log is for debugging grith itself. The audit log is the tamper-evident record of every decision it made, and it is the one with retention rules.
Operational log
| Where | What |
|---|---|
| stderr | Live output at general.log_level, default info |
~/.local/share/grith/supervisor.log | WARN and above, always |
~/.config/grith/tui.log | stderr while the grith exec TUI owns the terminal |
general.log_level takes trace, debug, info, warn or error. Override
it for one run with --log-level, or for a shell with GRITH_LOG_LEVEL.
supervisor.log holds its own file descriptor and captures WARN and above
whatever the log level says and whatever grith exec does to stderr. That is
deliberate: when the TUI is up, it is the only place warnings land. It is opened
in append mode and never rotated - if it grows, truncate it yourself.
Audit log
~/.local/share/grith/audit/audit.db, a SQLite database in WAL mode. The same
directory holds digest.db, writer.lock, and the cold/ archive directory.
general.audit_dir moves all of it.
One writer, enforced by the kernel. The daemon takes an exclusive flock on
writer.lock and holds it for its lifetime, so exactly one process can write,
verify-with-writes or run retention. Every other command opens the database
read-only and says so. That is why grith audit, grith log and grith digest
give you a read-only view while the daemon is up.
Every row is hash-chained. Each record carries its own hash, its predecessor's hash and a chain sequence number, so a removed or edited row shows up as a fork or a gap. Three frozen canonical hash forms are dispatched on each row's recorded version, which means a database written across several grith versions still verifies end to end.
⚠️A broken chain stops sessions
If the chain cannot be verified at startup, the daemon quarantines it: audit writes are refused
and no new supervised session is admitted. grith audit diagnose is the one audit command that
still works in that state. Startup never rewrites the chain to make a problem go away.
$ grith audit diagnose
Chain status: writable
Full verification: chain is empty.
Segments: 1 (continuous history)
Duplicate sequences: none.
Sequence gaps: none.
Assessment: no integrity problems detected.What gets recorded
audit.completeness sets how much routine activity is written:
| Value | Records |
|---|---|
decisions | Only proxy-evaluated events |
spawns | Plus a compact row per routine process spawn. Default |
io | Plus compact rows for routine file reads and writes, about +100 MB/day |
all | Plus noise-path events, about +500 MB/day |
Compact rows carry the call type, the supervised pid and tool, and a short argument summary; they omit the filter results, filter scores and composite score. The dashboard hides them until you turn on "Show routine activity".
A full row records the timestamp, session, tool call type, an argument summary and argument hash, the composite score, the proxy action and enforcement outcome, the per-filter results and scores, the decision reason, the supervised tool and pid, the project name, LLM provider, model, token counts and estimated cost, and the chain fields.
It does not record content. grith never traps read or write, so there is
nothing to record: a file write's content hash is a hash of the path, not of
the data. Prompts, model output and typed messages are not captured either.
Retention
Retention runs on its own thread inside the daemon, once at startup and then
every prune_interval_hours. None of these keys appear in the shipped
config/default.toml; they are code defaults you can set yourself.
[audit]
retain_full_days = 30 # 0 disables retention entirely
cold_storage_enabled = true # archive before deleting
prune_interval_hours = 24 # 0 = prune once on startup onlyA prune takes a contiguous prefix of rows older than the cutoff, writes them to
cold/YYYY-MM-DD.jsonl.zst as date-partitioned zstd-compressed NDJSON, deletes
them, and moves the chain checkpoint to the boundary hash so what remains still
verifies. Contiguous is the point: the single chain only supports prefix prunes.
The archive is written before the delete, so an interrupted prune leaves records
in both places rather than losing them.
retain_compact_days is parsed but not independently enforced, for the same
reason - the effective cutoff for every row is retain_full_days.
The same thread prunes the local analytics projection at 90 days, and it always lets the projection catch up before deleting any row, so nothing is pruned before it has been counted.
ℹ️Retention prunes on age alone
Earlier versions refused to prune records the cloud had not acknowledged. The raw audit-record sync is gone, so that guard is gone with it - v0.3.1 prunes purely on age. On older versions the guard silently disabled retention on any install with no cloud component, which is how a 1.4 GiB database happened.
With cold_storage_enabled = false, pruned rows are deleted with no archive and
a log line says so. Cold archives themselves have no automatic expiry: they are
yours to manage.
Reclaiming disk space
Deleting rows from SQLite does not shrink the file - freed pages go on an internal freelist and are reused by later writes. To rewrite the database and give the space back:
$ grith audit compactcompact rewrites into a temporary copy, verifies that copy's chain, and
atomically swaps it in; on any failure the original is untouched. It prints
before and after sizes. It is manual maintenance and never runs on a timer, and
it has two hard gates: no daemon may be running (it needs the writer lock), and
it refuses on a quarantined chain so evidence is preserved.
Export
$ grith audit export --format json --limit 5000 > audit.json--format takes json or csv, with --offset and --limit (default 0 and
1000, newest first). The CLI's CSV is a reduced five-column form - timestamp,
plugin, call type, decision, score. The dashboard's export endpoint gives the
full column set and can stitch cold archives back in.
For a live view of a session as it runs, use grith log --tail.
What leaves the machine
Audit records do not. There is no raw-record upload, and the local audit log is
the only place they exist. On a paid plan the daemon publishes an aggregated
analytics projection - decision counts, score distributions, per-filter activity,
sessions, model usage and cost - and never commands, paths, prompts or file
contents. Once a UTC day closes, that same day's aggregated rows are also
uploaded as a single daily archive file to encrypted private storage; what the
archive contains is stated in the consent summary before sync turns on.
general.audit_sync = false turns all of it off.
| Data | Kept for |
|---|---|
| Local audit database | retain_full_days, default 30 days |
| Local cold archives | No automatic expiry; yours to manage |
| Local analytics projection | 90 days |
| Cloud rollups and daily archives | 90 days |