Audit log vs encrypted spool
Control Zero writes audit data in more than one place, and the files serve
different purposes. This page explains what each file is, what guarantee it
gives, and -- importantly -- which one is the tamper-proof record. If you have
opened ~/.controlzero/audit.log and were surprised it is plaintext, this
page is for you.
The short version
| Surface | Encrypted? | Tamper-proof? | Purpose |
|---|---|---|---|
~/.controlzero/audit.log | No (plaintext) | No | Readable convenience copy for local inspection |
| Server-side audit store | In transit + at rest | Yes (hash chain) | The authoritative record shown in the dashboard |
~/.controlzero/spool/ | Yes (AES-GCM) | Yes (per-record auth tag) | Durable offline buffer that uploads to the backend |
The local convenience log: ~/.controlzero/audit.log
When the SDK records a decision it appends a line to a local, human-readable
audit log. In hosted mode (an API key is set) this file lives at
~/.controlzero/audit.log; in local-only mode it defaults to
./controlzero.log (configurable via log_path). The format is JSON Lines --
one JSON object per line:
{
"timestamp": "2026-04-15T12:00:00Z",
"tool": "llm",
"method": "generate",
"decision": "allow",
"policy_id": "rule-1",
"reason": "LLM calls are permitted",
"args_keys": ["model"],
"mode": "hosted"
}
This file is intentionally plaintext and unencrypted, even when an API key is configured. That is by design:
- It is a convenience copy so you can
tail -fyour governance decisions, grep them, or feed them into a local log pipeline without any extra tooling. - It is not the source of truth, and it is not tamper-proof. Anyone who can read the file can read it; the SDK does not sign or encrypt it.
- Sensitive values matched by DLP rules (PII, financial data) are stripped from this local copy before it is written -- the row records that a match fired (its rule id, category, and location) but not the matched value. The remote sink keeps full fidelity over TLS. So the local log is safe to read, but you should still treat it as you would any application log file.
If you need an authoritative, verifiable record, do not rely on this file. Use the server-side audit store described below.
The tamper-proof record
Tamper-proofing comes from two layers that work together, neither of which is the plaintext convenience log.
1. Server-side audit store with a hash chain
In hosted mode every decision is forwarded to the Control Zero backend, where it lands in the authoritative audit store. Each entry includes a hash of the previous entry, forming an append-only hash chain. The backend verifies chain continuity on ingest, so entries that were deleted, reordered, or modified in transit are detected and flagged. This server-side store is what the dashboard's audit viewer shows, and it is the record you should cite for compliance. See Tamper Detection and Enforcement for how the hash chain and tamper alerts work.
2. The local encrypted spool: ~/.controlzero/spool/
So that a backend outage or an offline machine never loses audit data, hosted (API-key) clients buffer audit to a durable, encrypted spool on disk before uploading. This is the durable-by-default behavior introduced in SDK 1.9.2.
- The spool lives at
~/.controlzero/spool/by default (override withCONTROLZERO_SPOOL_DIR). - Each record is serialized, encrypted with AES-GCM, and fsynced to an append-only write-ahead log before any network send. The per-record authentication tag and a hash chain mean a tampered or truncated frame is rejected, not silently accepted.
- A background drain uploads spooled records to the backend opportunistically. Audit is no longer lost if the backend is briefly unreachable.
The spool is the durable, encrypted hand-off to the authoritative store -- distinct from the plaintext convenience log, which is never uploaded.
How the spool is encrypted: the OS keystore and its fallback
The spool's data-encryption key (DEK) is stored in the operating system keystore by default:
- macOS: the Keychain (a
securitygeneric-password item). - Linux: the Secret Service (
secret-tool/ libsecret).
When the keystore holds the DEK, the on-disk spool.key file contains only a
sentinel -- not the real key. In that configuration, someone who can read the
spool directory can neither decrypt nor forge spooled records, because the
AES-GCM key never touches the disk.
Fallback to an on-disk key
The keystore is not always available -- for example on headless servers,
Windows, a locked keystore, or when the keystore CLI is missing. Keystore
access is strictly non-interactive and time-bounded, so it never blocks the
agent. When the keystore cannot be used, the SDK falls back to storing the DEK
in an on-disk spool.key file with 0600 permissions.
This fallback preserves durability -- audit is still encrypted on disk and still uploads -- but it weakens the local confidentiality and tamper guarantee: because the key and the ciphertext now sit in the same directory, anyone who can read the spool directory can read the key and therefore decrypt (and potentially forge) the spooled records. Key hardening never costs audit durability, but the on-disk-key mode is a weaker local posture than the keystore-backed mode.
You can control this behavior with environment variables:
# Require the OS keystore; do not fall back to an on-disk key
export CONTROLZERO_SPOOL_KEYCHAIN=1
# Force the legacy on-disk-key mode (skip the keystore)
export CONTROLZERO_SPOOL_KEYCHAIN=0
If you require keystore-backed confidentiality, run with
CONTROLZERO_SPOOL_KEYCHAIN=1 and ensure a keystore is present, or rely on the
server-side hash-chained store as your authoritative, verifiable record.
What to use when
- Quickly eyeball recent decisions on one machine: read
~/.controlzero/audit.log. Convenient, not authoritative. - A verifiable record for compliance or investigations: use the dashboard's audit viewer, backed by the server-side hash-chained store.
- Offline durability with encryption: the spool handles this automatically in hosted mode; verify the keystore is available if local confidentiality matters.