SecretSpec 0.12: audit logs and coding agents

domenkozar1 pts0 comments

SecretSpec 0.12: audit logs and coding agents | SecretSpec<br>Skip to content

SecretSpec 0.12: audit logs and coding agents

Jun 8, 2026<br>Domen Kožar

A coding agent reaches for the same secrets you do, but on its own initiative and<br>many times a session: a read looks identical whether it came from you running a<br>deploy or an agent exploring the codebase.

SecretSpec 0.12<br>makes that access accountable. It ships three things:

Audit log — every secret read and write is appended to a local,<br>per-user JSONL log. On by default. Values are never recorded.

Reason-on-access — secret access can require a human-readable reason,<br>enforced for coding agents by default.

secretspec audit command — filter and summarize the log, or pipe raw<br>JSON Lines to jq.

Behavior change in 0.12<br>If you run SecretSpec inside a coding agent, secret access now fails until a<br>reason is supplied. This is the new default (require_reason = "agents"). Opt<br>out with require_reason = false in the [project] table. Existing providers<br>and library callers keep working unchanged. See Upgrading.

The audit log<br>Section titled “The audit log”

Every secret read and write, from the CLI and the Rust SDK, is appended to a<br>local log as JSON Lines, one event per line. Secret<br>values are never written , only metadata: the secret name, the profile, the<br>provider that served it (with any embedded credentials redacted), the outcome,<br>the reason, and who was asking, including the detected coding agent.

"v": 1,

"ts": "2026-06-04T17:04:00.893Z",

"action": "get",

"project": "my-app",

"profile": "production",

"key": "DATABASE_URL",

"provider": "keyring://",

"outcome": "found",

"reason": "deploy web frontend",

"actor": { "user": "alice", "agent": "claude-code", "is_agent": true },

"version": "0.12.0"

The log lives in your per-user state directory<br>(~/.local/state/secretspec/audit.log) and is created readable only by you. Read<br>it with any tool, or use the new secretspec audit command for filtering and a<br>readable summary:

Terminal window# 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")'

It is configured in your user-global config<br>(~/.config/secretspec/config.toml), not the project’s secretspec.toml, so a<br>repository you clone can’t quietly turn off or redirect your audit log. The log is<br>a single file capped at 1 MiB, a size-bounded recent record rather than permanent<br>compliance history; forward it to a central system if you need that. To turn it<br>off entirely:

~/.config/secretspec/config.toml[audit]

enabled = false

See Audit Logging for the full record schema and options.

Supplying a reason<br>Section titled “Supplying a reason”

When a coding agent like Claude Code reaches for a secret without a reason, the<br>access is refused and the agent is told exactly what to do next:

$ secretspec run -- npm test

Error: Accessing secrets requires a reason. Provide one with --reason

"", the SECRETSPEC_REASON environment

variable, or Secrets::with_reason() in the SDK. (Policy: require_reason in

[project] of secretspec.toml — defaults to "agents"; set it to false to

disable.)

", the SECRETSPEC_REASON environmentvariable, or Secrets::with_reason() in the SDK. (Policy: require_reason in[project] of secretspec.toml — defaults to "agents"; set it to false todisable.)">

Claude Code reads that message, states why it needs the secret, and retries:

Terminal windowsecretspec run --reason "run the test suite before opening a PR" -- npm test

Both the refusal and the successful retry land in the audit log, so the reason<br>is tied to the access. There are three ways to supply a reason:

SourceScopePrecedence--reason flagCLIhighestSecrets::with_reason()SDKoverrides envSECRETSPEC_REASONCLI + SDK + derivelowest<br>Terminal window# CLI: the most explicit option, overrides the others

secretspec run --reason "deploying release 0.12" -- ./deploy.sh

// SDK: the programmatic equivalent of --reason

let secrets = Secrets::load(/* ... */)?.with_reason("nightly backup job");

Terminal window# Env: lowest precedence, but honored everywhere

export SECRETSPEC_REASON="nightly backup job"

SECRETSPEC_REASON is resolved by Secrets::load / load_from, which means<br>secretspec-derive-generated code and other library callers satisfy the policy<br>and supply an audit reason without any code changes .

Whichever path you use, blank or whitespace-only reasons are ignored, so they<br>can’t quietly satisfy the policy. Under the hood this is backed by a new<br>Provider::set_reason trait method (a no-op by default), so existing providers<br>keep working unchanged.

Configuring when a reason is required<br>Section titled “Configuring when a reason is required”

The new require_reason policy in the [project] table controls when a reason<br>is mandatory:

[project]

name = "my-app"

require_reason = "agents" # require it from agents (default), or true /...

secretspec reason audit agents project coding

Related Articles