grithdocs

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

WhereWhat
stderrLive output at general.log_level, default info
~/.local/share/grith/supervisor.logWARN and above, always
~/.config/grith/tui.logstderr 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.

terminal
$ 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:

ValueRecords
decisionsOnly proxy-evaluated events
spawnsPlus a compact row per routine process spawn. Default
ioPlus compact rows for routine file reads and writes, about +100 MB/day
allPlus 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.

toml
[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 only

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

terminal
$ grith audit compact

compact 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

terminal
$ 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.

DataKept for
Local audit databaseretain_full_days, default 30 days
Local cold archivesNo automatic expiry; yours to manage
Local analytics projection90 days
Cloud rollups and daily archives90 days

See also

Last updated: 2026-08-24Edit this page on GitHub →