Audit Logging
secretspec records every secret access to a local audit log so you can review, after the fact, what secret was accessed, when, by whom, with what reason, if supplied, which software integration called SecretSpec (0.20+), and what the outcome was. Auditing is on by default.
Secret values are never written to the log. Only metadata is recorded, and any credentials embedded in a provider URI are redacted.
Where the log lives
Section titled “Where the log lives”By default the log is written to the per-user state directory, one entry per line in JSON Lines format:
| Platform | Default path |
|---|---|
| Linux | ~/.local/state/secretspec/audit.log |
| macOS | ~/.local/state/secretspec/audit.log |
(secretspec follows the XDG state-directory convention on macOS too, matching
where it keeps its config, so the path is the same as on Linux. Set [audit] path to override it.)
The file is created with owner-only permissions (0600 on Unix), inside an
owner-only directory (0700). The first time
secretspec writes to it, it prints a one-time note telling you where the log is
and how to turn it off.
What a record looks like
Section titled “What a record looks like”{ "v": 1, "id": "386987e6-291f-4e8f-a08b-73db9d80897b", "ts": "2026-06-04T17:04:00.893Z", "session_id": "d59e0f0f-ed2f-456f-a2b6-be25a24b7ec7", "seq": 0, "action": "get", "project": "my-app", "profile": "production", "key": "DATABASE_URL", "provider": "keyring://", "outcome": "found", "reason": "deploy web frontend", "caller": { "name": "git", "version": "2.51.0", "operation": "credential_get", "resource": "github.com" }, "actor": { "user": "alice", "agent": "claude-code", "is_agent": true }, "version": "0.20.0"}| Field | Meaning |
|---|---|
v | Schema version of the record |
id | Unique id for this event |
ts | RFC 3339 UTC timestamp |
session_id | Shared by every event from one secretspec invocation |
seq | Monotonic sequence within that invocation |
action | The operation: get, set, check, run, import, export, cache_clear / cache_refresh (0.17+), or delete (0.18+) |
project / profile | The project and profile in effect |
scope | The named scope for a scoped check, run, or export; omitted otherwise (SecretSpec 0.17+) |
key | The secret name for single-secret actions (get/set, and delete in 0.18+); never its value |
keys | The set of secret names for bulk actions (check/run/import/export) |
command | For run, the executed program (argv[0] only — never its arguments, which may contain secrets) |
provider | The provider URI that served the access, with credentials redacted |
outcome | found, missing, default, written, deleted (0.17+ cache clear), started (a run launched its command), or error |
A cached route writing its local entry is recorded as cache_refresh/written, never as set: no authoritative store was written. Dropping an entry — cache clear, or an entry a write superseded — is cache_clear/deleted. | |
error_kind | A non-sensitive tag when outcome is error |
reason | The reason supplied via --reason / SECRETSPEC_REASON / the SDK, if any |
caller | Caller-asserted software integration context: name, and optional version, operation, and non-secret resource (SecretSpec 0.20+) |
actor | The OS user, the detected coding agent (if any), and whether this is an agent session |
This pairs naturally with the require_reason
policy: when that policy applies, SecretSpec requires the caller to state why
before proceeding and records the supplied reason alongside the access.
Caller context answers what software requested access; reason answers why
the user requested it. Caller context is informational, is not an authenticated
identity, and never satisfies require_reason. Integrations must not place a
credential or secret value in any caller field.
Reading the log
Section titled “Reading the log”The log is plain JSON Lines, so any tool works (cat, tail -f, jq). The
secretspec audit command reads it for you with filters
and a readable summary:
# Last 20 entries, formatted$ secretspec audit -n 20
# Only `run` events for one project$ secretspec audit --project my-app --action run
# Raw JSON Lines, piped to jq$ secretspec audit --json | jq 'select(.outcome == "missing")'Size cap
Section titled “Size cap”The log is a single file capped at 1 MiB by default. When it reaches the cap it is truncated and started fresh, so disk usage stays bounded without any log rotation to manage. This makes the log a size-bounded recent record rather than a complete, permanent history — it is not intended to satisfy long-term compliance retention on its own. Forward it to a central system if you need that.
Reliability
Section titled “Reliability”Auditing never blocks secret access. If the log cannot be written (for example, a
read-only filesystem), secretspec prints a warning: to stderr and continues —
your get, set, and run still work.
Configuration
Section titled “Configuration”Auditing is a per-machine concern, so it is configured in your user-global
config (~/.config/secretspec/config.toml) under the top-level [audit] table —
not in the project’s secretspec.toml. This means a repository you clone cannot
turn off or redirect your audit log. See the
configuration reference for all options.
To turn it off:
[audit]enabled = false