Skip to main content
CC Safety Net writes a structured audit trail of command decisions. Use it to review what your agent tried to do and what happened. Logs use JSON Lines (JSONL), with one JSON object per line. CC Safety Net stores these logs on your machine. This page defines the file layout, record schema, scope, retention, and redaction limits. To read the log in a UI, see Dashboard. To read it in the terminal, see logs.

Log layout

Records are written to a per-project, per-month path:
The session id is sanitized before it reaches the filename: runs of characters outside A-Za-z0-9_.- collapse to _, leading and trailing ., -, and _ are stripped, and the result is truncated to 128 characters. If sanitizing leaves an empty string, ., or .., the write is abandoned entirely — this is the path-traversal defense, not a formatting nicety. Directories are created with mode 0700 and the log file is appended with mode 0600. Write failures are ignored. Audit logging cannot change an allow or block decision.
Flat files directly in ~/.cc-safety-net/logs/ are the legacy layout from earlier versions. They are still read by logs and still swept by retention, and they are what logs --prune-legacy targets. New records are never written there.

Each record contains one decision

Each line records exactly one allowed-or-blocked command decision: the command, the segment that drove the decision, the reason, and the rule that matched.
  • No command output, model prompt, tool result, or conversation content is read or stored anywhere in the write path.
  • Denials are always recorded.
  • Allowed decisions are recorded only when the tool call actually routed to a command. An allowed non-command tool call produces no record at all.
  • Blocks that came from the fail-closed path — the analyzer errored and the guard refused rather than guessed — are recorded, and are marked with failureStage and errorCode precisely so you can find them. logs --suspect is built on that field.

Record schema

Example record:

Length caps and the truncation indicator

Caps are applied after redaction, so redaction is never cut off part-way through a token. If any one of command, segment, toolName, or cwd exceeded its cap, the record gains truncated: true. The flag is never written as false — its absence means nothing was cut. logs --id renders it as truncated: yes or -.

What is recorded: audit scope

CC_SAFETY_NET_AUDIT_SCOPE decides whether allowed command decisions join the denials in the log. An invalid value is not silent. doctor raises the finding environment.audit-scope-invalid at warning severity — “Audit scope value is invalid” — with the fix hint to set the variable to all or blocked and restart the integration. The offending value itself is deliberately not echoed back. Denials are never suppressed by scope. Scope only ever gates the allow branch.

Retention

audit.retention_days must be a whole number between 1 and 365; anything else is rejected by validation, and any value that cannot be used at all falls back to the 30-day default. Retention is read straight out of the policy file on its own, so pruning keeps working even when the rest of the policy fails validation. The same value bounds the logs --since ceiling and the windows the GUI Activity view offers.
Shortening retention is irreversible. The sweep recomputes its cutoff on every run, so lowering the value makes already-existing records immediately eligible for deletion and they are unlinked — there is no archive, no trash, and no undo. The GUI asks you to confirm before lowering the value for exactly this reason.

Pruning is opportunistic

Nothing runs on a timer. The retention sweep is triggered by activity:
  • after every audit write (deliberately after, so a pruning failure can never cost the record),
  • before logs reads,
  • before doctor builds its activity summary,
  • before the GUI activity feed loads.
The sweep traverses at most once per UTC day per audit root, throttled by a zero-byte .last-prune marker in the audit root. It never throws, never creates the audit root, never follows symlinks, and leaves any file shape it does not recognize untouched. Empty month directories and empty project directories are reclaimed, except the current month, which is left alone to avoid racing an in-flight write. Legacy flat files are deleted only when both the file’s timestamp and every record inside it prove it wholly expired; a file with mixed ages is never rewritten or split. Expired records can remain on disk while CC Safety Net is idle because nothing starts the sweep. logs --id searches records that are present on disk. It can return a record past its retention window if pruning has not reached it.

Deleting legacy logs

cc-safety-net logs --prune-legacy deletes every legacy flat *.jsonl file in the audit root immediately and irreversibly. There is no confirmation prompt and no --yes flag; the only preview is --dry-run, which reports exactly the set that would be deleted and deletes nothing. Membership is decided by file position alone — age, schema validity, and malformed lines are all irrelevant.
This is not the retention sweep. The retention sweep deletes legacy files only when they are wholly expired; --prune-legacy deletes them regardless of age. Nested per-project logs are never entered and are never touched, and the command says so in its output. For the flag’s exit behavior and the combinations it rejects, see logs --prune-legacy.

Counts and returned entries

Counts cover the full window. Entry lists are capped, so the two numbers can differ. When logs scans the log, every source it has to drop — an unreadable directory, an unreadable file, or a malformed record — is counted, and one warning goes to stderr: warning: N audit log sources could not be read; these results are incomplete (source when N is 1). No paths are named, and stdout and the exit code are unchanged, so --json output stays parseable. A missing logs directory is an empty history, not a dropped one, and produces no warning. Normal list and GUI windows do not look further back than retention. The GUI derives its window choices from your retention value rather than offering a fixed list. A direct logs --id lookup can still return an expired record while it remains on disk and before pruning removes it. See Dashboard for the choices available at each retention setting.

Secret redaction

Command, segment, tool name, and working directory are passed through secret redaction before the record is serialized. Recognized values are replaced with <redacted>:
  • Environment assignments whose name contains TOKEN, SECRET, PASSWORD, PASS, KEY, or CREDENTIALS
  • Database connection variables (DATABASE_URL, POSTGRES_URL, MYSQL_URL, REDIS_URL, MONGODB_URL, and other DSN/URL/URI/connection-string variables)
  • PEM private key blocks (-----BEGIN ... PRIVATE KEY-----)
  • Secret-bearing HTTP headers (Authorization, Cookie, X-API-KEY, API-KEY)
  • URL credentials (scheme://user:pass@host and scheme://token@host) and -u user:pass
  • Presigned-URL signature query parameters — the values of x-amz-signature, x-goog-signature, sig, and signature, matched case-insensitively when the parameter name follows the start of the text, whitespace, ?, &, ;, or |
  • A fixed list of provider token formats (ghp_..., gho_..., xoxb-..., npm_..., sk_live_..., rk_live_..., pypi-..., and similar)
  • JWTs (eyJ...) and AWS access key ids (AKIA... / ASIA...)
Redaction is bounded. It is a fixed list of patterns, not a classifier. Everything it does not recognize is retained verbatim: absolute filesystem paths, project and directory names, hostnames, IP addresses, usernames, ticket ids, filenames, and any credential whose shape is not on the list. The reason field is not redacted at write time at all. Treat the audit log as sensitive local data, and review any excerpt before pasting it into an issue or a chat.
Before the GUI sends a false-positive report, it removes your home-directory prefix. Other paths can remain in the report. Permissions (0700 on directories and 0600 on files) and local storage limit access. They do not redact the log or make it safe to share.
  • CLI commands — the logs command, its filters, and its JSON output.
  • Dashboard — reading the same records in the GUI Activity feed.
  • Policy — the audit.retention_days field and its validation.
  • Explain trace — the same redaction bound applies to explain output.
  • Security model — where the audit log sits in the overall threat model.
Last modified on August 12, 2026