Skip to main content
policy.json is CC Safety Net’s settings file. It selects your safety preset, turns individual built-in protections on or off, adds extra protected paths, and sets how long audit records are kept. It has two scopes: your user file, and an optional project file committed to a repository. It is separate from rule.json and rulebooks, which define your own custom blocking rules. See Custom rules for that schema.

Policy file location

With CC_SAFETY_NET_HOME set, the user file sits directly under that directory, as a sibling of rules/. See Environment for the override itself. CC_SAFETY_NET_HOME governs the user file only. The project file is always .cc-safety-net/policy.json under the project root, the same directory the rules scope resolves. The runtime reads both files on every tool call. The user file is the baseline and the project file layers on top of it. See Project policy. When the dashboard writes the user file it creates the directory with 0700 and the file with 0600.

Project policy

A team ships a safety policy through the repository by committing .cc-safety-net/policy.json. Members run nothing. The runtime reads the file on the next tool call, in every checkout. The project file is sparse. Only the fields it sets are written, and an absent field keeps inheriting from the user policy. A project file that sets one rule override changes that one rule and nothing else. The effective policy is the user policy with the project policy layered on top: An audit section in a project file is ignored, and the load reports project policy audit settings are ignored; audit is user scope only.

Reported weakenings

A project policy is honored as written. Nothing clamps it back up to the user baseline. Instead, every field it relaxes relative to the user policy gets its own reported line:
  • project policy lowers level: <user> -> <project>
  • project policy disables fail_closed, project policy disables paranoid_rm, project policy disables paranoid_interpreters
  • project policy enables worktree mode relaxations
  • project policy disables destructive command protection
  • project policy disables secret protection
  • project policy disables rule <id>
  • project policy adds destructive allow path: <path>
  • project policy adds secret allow path: <path>
They appear in four places:
  • status adds a Project row with the project file’s path, and a Project policy block listing the lines.
  • The status line adds a 🔻 glyph while any weakening is in force. See Status line.
  • doctor prints a Project policy deltas: block under Effective Safety.
  • doctor and explain name the scope that supplied the safety preset next to it, as user policy, project policy, or built-in default. See Explain trace.

Policy file protection

policy.json is a protected path in both scopes and in every runtime state, ready or degraded. Policy-file protection runs before the policy snapshot is loaded, so a broken config cannot weaken it. These operations hard-stop:
  • Writes, edits, and patches to the file through any tool
  • Shell commands naming the file as an operand
  • Write redirections into the file
  • Recursive rm of its directory or any ancestor
  • find … -delete and find … -exec rm reaching it
  • mv with the file, its directory, or an ancestor as the source
The two scopes protect different directory chains. The user file contributes its own directory and every ancestor. The project file is matched at two locations, resolved from the execution working directory and from the config working directory, and each one contributes only its own .cc-safety-net directory. The chain stops there on purpose. Walking further up would claim the working directory and every ancestor above it, and since this guard runs first, rm -rf . and find . -delete would report this generic reason instead of the specific one their destructive-command rule gives them. Reading stays allowed. A read-only command whitelist covers [, cat, file, grep, head, jq, less, ls, more, rg, sed, stat, tail, test, and wc. It covers sed only when the command does not edit in place with -i or --in-place. Read-only tools such as Grep and Glob are exempt entirely.
Because your agent cannot write this file, ask it to show you the change instead. Apply policy edits yourself in an editor, through the dashboard, or with policy apply.

Edit the policy file

Choose one of these options:
  • Use the dashboard. Run cc-safety-net gui. The dashboard writes the file with the correct permissions and can repair a file that does not validate. While the file has an error, the form shows the full defaults instead of the valid values from your file. You cannot save until you repair the file. Repair keeps each recognized valid setting and discards invalid fields. If you do not want this repair behavior, edit the file by hand and check it with status.
  • Edit the JSON directly. Open the file in an editor and change it by hand. The runtime reads the change on the next tool call. You do not need to restart anything.
  • Apply a proposal. Run cc-safety-net policy apply <file> to write a proposal file into either scope after you confirm the diff in a terminal. See Check and apply a proposal.
After a hand edit, confirm the result:
A degraded verdict means part of your file was rejected. npx cc-safety-net doctor names exactly which fields. The runtime never rewrites policy.json on its own, so an invalid file stays exactly as you left it until you fix it or use the dashboard repair action.

Check and apply a proposal

policy check and policy apply split one flow between the agent and you. The agent writes a proposal JSON and runs policy check to show what it would change. You run policy apply yourself, in a terminal.
Both subcommands print the same header and diff before anything is written:
Project scope diffs the effective merge, before against after. The Effective policy (user + project merged): line marks that. A sparse proposal moves the level the session actually runs at, so the confirmation shows the merged result rather than the file’s own contents. With -g, --global the header reads Scope: user (<path>), there is no merge line, and the diff compares the user file against the proposal with audit.retention_days included. Diff rows read <field>: <before> -> <after> under a Changes (<n>): heading. An absent side renders as (unset). With nothing to change, the command prints No changes.. An audit section in a project proposal is rejected before the diff, with <file>: audit settings are user scope only; remove the audit section from a project proposal. policy apply needs a TTY on both stdin and stdout. Without one it prints the command for you to run and exits 1:
In a terminal it asks Apply this policy to <path>? [y/N] . Only y or yes accepts, in any case; anything else declines, and so does EOF. A decline prints Cancelled; nothing was written. and exits 0. A write prints Policy applied: <path>. There is no --yes flag and no non-interactive mode. An agent that runs policy apply is denied with a hard stop, whose reason is exactly:
The recognizer over-matches on purpose. It covers direct invocation, npx, bunx, pnpx, pnpm dlx and yarn dlx, npm exec, pnpm exec and yarn exec, versioned specs such as cc-safety-net@latest, and bun or node running the entrypoint file, including runner options placed in front of the target. policy check stays allowed for agents.

Complete policy example

Every field, with its default value:
Only version is required. Every other field can be omitted, and an omitted field takes the default shown above. If the file does not exist at all, CC Safety Net runs on these defaults and stays ready. The root object is strict: an unrecognized top-level key is an error, as is an unrecognized key inside safety, workflow, destructive_command_protection, secret_protection, or audit.

Schema reference

integer
required
Schema version. Must be 1. This is the only required field; the diagnostic for a missing or wrong value is version must be 1.
string
default:"standard"
The safety preset. One of "standard", "strict", or "paranoid". Each preset supplies inherited capability defaults: strict enables fail_closed; paranoid enables fail_closed, paranoid_rm, and paranoid_interpreters. See Modes for what each capability changes.
boolean
Set the fail-closed capability explicitly, up or down, regardless of the preset. Omit the key to inherit from the preset.
boolean
Set the paranoid rm capability explicitly, up or down. Omit the key to inherit from the preset.
boolean
Set the paranoid interpreter capability explicitly, up or down. Omit the key to inherit from the preset.
boolean
default:"false"
Relax local-discard git rules inside a confirmed linked worktree. Detection is fail-closed: if the working directory cannot be positively identified as a linked worktree, the stricter default rules stay in effect. See Modes for the exact list of what is relaxed and what is never relaxed.
boolean
default:"true"
Master switch for the registered destructive-command rules. Setting it to false short-circuits every registered rule except the catastrophic rules, which are always enforced.
object
default:"{}"
Per-rule state, keyed by registered destructive-command rule id, with the value "on" or "off". Applied on top of the capability-derived state, so "on" can enable a rule your preset leaves off and "off" can disable one it turns on.
string[]
default:"[]"
Paths exempted from destructive-command rules. Entries must be absolute or start with ~/.
boolean
default:"true"
Master switch for secret protection. Setting it to false skips the entire secret stage, including your deny_paths.
object
default:"{}"
Per-rule state, keyed by registered secret-protection rule id, with the value "on" or "off". Most secret rules are on whenever secret protection is enabled, so "off" is the usual direction. The Coding CLI config tier is off by default, and an explicit "on" is how you opt into one of those rules.
string[]
default:"[]"
Extra paths to protect as secrets, in addition to the built-in sensitive paths. See Deny paths for the validation rules.
string[]
default:"[]"
Exact files or directory trees exempted from built-in secret-pattern rules. Configured deny paths and Coding CLI protections still apply. See Secret allow paths for the validation rules and precedence.
integer
default:"30"
Days of audit history to keep before the sweep deletes it. Must be an integer between 1 and 365.

Safety level and capability overrides

safety.level picks a preset. safety.overrides then sets individual capabilities explicitly. This is the only place that can turn a capability down. Environment flags can only raise it.
That example takes the paranoid preset but leaves interpreter one-liners alone. The reported effective level becomes custom whenever the resulting combination of capabilities matches no preset.
The environment can raise your policy’s level and force capabilities on, but never the reverse. See Environment for the full ordering between policy.json and the environment, including the worktree_mode OR and the legacy SAFETY_NET_* aliases.

Destructive-command protection

destructive_command_protection.overrides addresses built-in rules by id, for example:
An id that is not registered is rejected with unknown destructive command rule id "<id>", and any value other than "on" or "off" is rejected with destructive_command_protection.overrides.<id> must be "on" or "off". Catastrophic rules are always enforced and are not user-configurable. They ignore enabled: false and ignore an "off" override. These are the rules covering removal of / or your home directory and deletion of Git metadata, together with their PowerShell and find equivalents. See Blocked commands for the behavior each rule enforces.

Allow paths

destructive_command_protection.allow_paths exempts specific locations from destructive-command rules. Validation is stricter than for deny paths:

Secret protection

Secret protection blocks reads and writes of credential-bearing files. This section defines the configuration contract. The Secret protection reference lists every built-in rule id, the paths each rule protects, and the exemptions. secret_protection.overrides addresses individual built-in rules by id, with the value "on" or "off". "off" disables a rule that is on by default; "on" opts into a rule from the default-off tier:
An unregistered id is rejected with unknown secret protection rule id "<id>", and any other value with secret_protection.overrides.<id> must be "on" or "off".

Rules that are off by default

Most built-in secret rules are on whenever secret protection is enabled. One tier is not: the Coding CLI config rules, which cover the settings and MCP configuration files of supported coding agents. Those files can carry credentials inline, but agents also edit them as routine work, so the tier ships off and you opt in per rule with an explicit "on" override. A config rule you turn on protects the agent’s user-level config files, and it also protects project-level files matched by name at any repository root, such as any .mcp.json. The ten default-off ids, their on-by-default Coding CLI credential counterparts, and the exact paths each rule protects are listed in the Secret protection reference.

Deny paths

secret_protection.deny_paths adds your own protected locations on top of the built-in sensitive paths. Deny paths are checked first, before the built-in rules, and a hit is a hard stop attributed to the rule id secret.deny-path. Validation: Relative entries are accepted because they resolve against each session’s working directory, which is unknown when the file is saved. Home, anything above home, and / are rejected because they would block almost every command in every workspace under home. What a valid deny path protects: the path itself and every descendant. A target is normalized against the execution working directory and each configured path against the config working directory before comparison. Two limits worth knowing:
  • Deny paths only apply while secret_protection.enabled is true. Setting it to false turns them off along with everything else in the secret stage.
  • secret.deny-path is not a registered secret rule id, so secret_protection.overrides cannot disable it. Only secret_protection.enabled: false does.

Secret allow paths

secret_protection.allow_paths exempts an exact file or a directory and all its descendants from the built-in basename, home-directory, key-variant, and extension rules. Use it for files you manage deliberately, such as a repository’s .env.test, or a fixture directory that contains non-secret credential-shaped filenames. The precedence is fixed:
  1. A configured deny path always wins. If the same target is in both lists, the result is secret.deny-path.
  2. Coding CLI credential and config rules (secret.cli.*) always win. An allow path cannot expose the agent’s own credentials or configuration.
  3. An allow-path match suppresses any other built-in secret rule for that target.
Validation: Entries are literal paths, not glob patterns. The target is resolved from the execution working directory, and a relative allow entry is resolved from the config working directory. Both sides follow existing symlinks before the same-or-descendant comparison, so an allow root also covers a target reached through a symlink. The runtime repeats the safety-boundary checks after resolution. An entry that resolves to home or an ancestor of home is ignored. A target under the effective guard configuration root is never exempted, including when CC_SAFETY_NET_HOME moves that root or when home or ~/.cc-safety-net is a symlink.

Audit retention

audit.retention_days controls how long audit records survive before the retention sweep deletes them. The default is 30 days, and the accepted range is 1 to 365.
Pruning is opportunistic: it runs at most one traversal per audit root per UTC day, after audit writes and before audit reads. It never throws and never follows symlinks. See Audit log for the record schema and what is captured.
Retention resolves independently of the rest of the policy. The sweep reads this one field directly from the file, so a policy that fails validation elsewhere still prunes. A value that is missing, non-integer, or unusable falls back to 30; a value below 1 is clamped to 1, and above 365 to 365.Out-of-range values therefore do two things at once: the schema rejects them, degrading the runtime, while the sweep clamps them. "retention_days": 1000 both shows up as a diagnostic and prunes at 365 days.

Invalid policy behavior

An invalid policy.json never blocks ordinary work. It moves the runtime to degraded and uses a fallback. This table describes one file. Both scopes go through the same salvage, and each diagnostic names the file it came from with a path prefix. Salvage is deliberately protective, which is why a broken file usually produces more denials than you configured: A malformed project file is salvaged the same way. Its rejected sections drop, and everything else in both files stays in force. An unreadable user file normally falls back to built-in defaults. When a project file still contributes fields, those fields are in effect, so the state reports as salvaged rather than defaults. Scope provenance appears whenever the project file exists, valid or not, so status keeps printing its Project row.
Invalid entries are discarded, not repaired. A mistyped deny path silently stops protecting that location, and an invalid safety.level silently lowers you to standard. These are the two quiet failure modes. Run npx cc-safety-net status after every hand edit.
Configuration recovery is the complete contract for degraded state, including how it is reported and how to get back to ready.

Modes

What each safety capability changes, and what worktree mode relaxes.

Environment

Every variable, including the ones that raise your policy’s level.

Custom rules

The separate rule.json and rulebook schemas for your own blocking rules.

Configuration recovery

Ready versus degraded, the fallback matrix, and the repair sequence.
Last modified on August 31, 2026