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
failureStageanderrorCodeprecisely so you can find them.logs --suspectis 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.
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
logsreads, - before
doctorbuilds its activity summary, - before the GUI activity feed loads.
.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
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, orCREDENTIALS - 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@hostandscheme://token@host) and-u user:pass - Presigned-URL signature query parameters — the values of
x-amz-signature,x-goog-signature,sig, andsignature, 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...)
0700 on directories and 0600 on files) and local storage limit access. They do not redact the log or make it safe to share.
Related pages
- CLI commands — the
logscommand, its filters, and its JSON output. - Dashboard — reading the same records in the GUI Activity feed.
- Policy — the
audit.retention_daysfield and its validation. - Explain trace — the same redaction bound applies to
explainoutput. - Security model — where the audit log sits in the overall threat model.