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. Use Dashboard to read the log in a UI or logs to read it in the terminal.

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 a path-traversal defense, not a formatting rule. 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 from the fail-closed path are recorded. These occur when the analyzer errors and the guard refuses rather than guesses. The failureStage and errorCode fields identify them, and logs --suspect uses those fields.

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. An entry that carries a failureStage is the primary record for diagnosing a fail-closed event, so it stores the whole command instead of a capped one. The tool-input byte caps already bound that length upstream. The segment, toolName, and cwd caps are unchanged and apply to failure entries too. If command, segment, toolName, or cwd exceeds its cap, the record gains truncated: true. On a failure entry the command has no cap to exceed, so a long command there never sets the flag by itself; segment, toolName, and cwd still can. 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 environment.audit-scope-invalid at warning severity with the exact message “Audit scope value is invalid” and a hint to set the variable to all or blocked, then restart the integration. It does not echo the offending value. 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 existing records immediately eligible for deletion. The sweep unlinks them with no archive, trash, or undo. The GUI asks you to confirm before lowering the value.

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 the files that would be deleted and deletes nothing. File position alone decides membership. Age, schema validity, and malformed lines do not matter.
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. It never enters or changes nested per-project logs, and its output states this. See logs --prune-legacy for exit behavior and rejected option combinations.

Counts and returned entries

Counts cover the full window. Entry lists are capped, so the two numbers can differ. When logs scans the log, it counts each dropped source: an unreadable directory, an unreadable file, or a malformed record. It writes one warning to stderr: warning: N audit log sources could not be read; these results are incomplete (source when N is 1). The warning names no paths. Stdout and the exit code remain unchanged, so --json stays parseable. A missing logs directory means empty history, not a dropped source, 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 documents the logs command, filters, and JSON output.
  • Dashboard shows the same records in the GUI Activity feed.
  • Policy defines and validates audit.retention_days.
  • Explain trace uses the same redaction bound for explain output.
  • Security model places the audit log in the threat model.
Last modified on August 31, 2026