Skip to main content
Use custom blocking rules to enforce team conventions or project-specific safety policies. Rules use a rulebook-based layout and merge from user and project scopes. This lets you keep personal defaults with project overrides.
Breaking change. CC Safety Net no longer loads legacy inline configuration files (.safety-net.json and ~/.cc-safety-net/config.json) at runtime. If they contain rules, those rules are inert until you migrate. The runtime ignores legacy files silently. rule verify warns about them. Ordinary commands keep working. Run npx -y cc-safety-net rule migrate to convert legacy rules into the rulebook layout. See Migrate legacy configuration.
Authoring rules is one workflow of the /cc-safety-net skill. Run it inside your agent and describe what you want in natural language:
The /cc-safety-net skill page covers its other workflows. If your agent does not support skills, prompt it with:

Rule configuration file locations

CC Safety Net loads rulebooks from two scopes and merges them:
  1. User scope. ~/.cc-safety-net/rules/rule.json (created with rule init --global). Use this for personal defaults that apply to every project.
  2. Project scope. .cc-safety-net/rules/rule.json in the project root. Use this for team or project-specific rules you can commit to source control.
Local rulebook sources are referenced by bare names like project-rules. GitHub rulebook sources use owner/repo#ref/<rulebook-name> and point to .cc-safety-net/rules/<rulebook-name>/rulebook.json in that repository. rule add and rule update vendor that file into your own scope at the same relative path, so every active rulebook is a file in the config directory.

Scope merge behavior

  • Rulebooks from both scopes are combined, user scope first.
  • Duplicate active rulebook names resolve by first claim. CC Safety Net loads user scope first, so a name it claims shadows the project rulebook with the same name. The later rulebook contributes no rules instead of partially shadowing the first. CC Safety Net reports the collision as a warning and puts the runtime in the degraded state. Rename one of the rulebooks in its rulebook file and in the rule.json that lists it. Because the collision is resolved instead of fatal, adding or updating a source in one scope still succeeds when the other scope already uses that name.
  • Each scope’s overrides apply to that scope’s own rules. CC Safety Net ignores a project override that names a user-scoped rule and reports a warning. The rule keeps its user-configured state. Project config cannot disable or rewrite a user rule.
  • An override key that matches no known rule is ignored with a warning; other overrides and rules keep their configured state.
  • transparent_wrappers from both scopes are unioned.
If no config is found in either location, only built-in rules apply.

Managing rulebook sources

A rulebook source is referenced by an entry in rule.json’s rules array. There are two kinds:
  • Local source. A bare name such as project-rules. The rulebook lives at .cc-safety-net/rules/project-rules/rulebook.json (project) or ~/.cc-safety-net/rules/project-rules/rulebook.json (user). Local sources must stay within their config directory.
  • GitHub source. owner/repo#ref/<rulebook-name> names .cc-safety-net/rules/<rulebook-name>/rulebook.json in that repository and ref. rule add and rule update vendor that file into your own scope at .cc-safety-net/rules/<rulebook-name>/rulebook.json, writing the fetched bytes verbatim, so it loads exactly the way a local rulebook does.
Use the rule command to add, update, and remove sources rather than editing rule.json by hand:
Add --global (-g) to operate on the user scope instead of the project scope. rule list is the exception. It reads both scopes and rejects --global. See CLI commands for every rule subcommand, its options, and its exit behavior.

Install rulebooks from a repository

rule add accepts a bare owner/repo as well as the canonical owner/repo#ref/<rulebook-name>. A bare repository adds every rulebook that repository publishes under .cc-safety-net/rules/. Two flags narrow that down, and both work only on rule add with an owner/repo source:
  • --only <rulebook...> takes one or more rulebook names and keeps the order you list them in.
  • --ref <ref> picks a branch, tag, or commit instead of the repository’s default branch. A ref may contain / segments, so --ref feature/rulebook-v2 works.
rule.json stores the canonical form owner/repo#ref/<rulebook-name>, keeping the ref you asked for rather than the commit it resolved to. That ref stays movable. The resolved commit is reported instead of stored, so an add prints Vendored at 1a2b3c4. and writes the rulebook file, and there is no lockfile to pin it. rule update re-resolves every selected source, so a branch or tag ref follows wherever it now points and the vendored file is rewritten from there. Sources update independently. One that fails to fetch or validate keeps its vendored copy and is reported as Failed to update <spec>: <message> while the others still update. A resource-limit failure is the exception and stops the whole run.

Resource limits

The rules array holds at most 64 sources per scope. A rule.json with more entries fails validation with the single error Rule config exceeds CC Safety Net's safe source limit. The error does not list each item in the oversized array, and CC Safety Net drops the whole scope’s config like any other invalid rule.json. Fetching from GitHub runs under fixed budgets. rule add and rule update process at most 4 sources concurrently, and one run makes at most 131 GitHub requests and reads at most 64 MiB of response bytes across all sources. Exceeding a budget stops the run with Rule synchronization exceeds CC Safety Net's safe resource limits. and fails every source in it, not only the one that exceeded the budget. Rulebook files have their own limits, checked before the schema. A rulebook over any of them is rejected with the single error Rulebook exceeds CC Safety Net's safe validation limits. and no per-field detail: A rulebook that clears those limits and then fails the schema reports at most 64 errors, followed by Additional rulebook validation errors were omitted.

Rulebooks are live files

There is no lockfile, no digest, and no cache. Every source loads from <config-dir>/<rulebook-name>/rulebook.json, and the runtime reads that file on every tool call. A local rulebook is authored there directly; a remote one is vendored there by rule add and rule update. A saved edit applies to the next command, so there is nothing to publish or rebuild afterwards. A source whose rulebook.json is missing, unreadable, or invalid is inactive. It contributes no rules, every other source and every built-in protection keeps applying, ordinary commands keep running, and the runtime reports degraded. An unreadable or invalid rule.json makes every source in its scope inactive. For a GitHub source that has not been vendored yet, run npx -y cc-safety-net rule update. See Configuration recovery for the full state model, the exact diagnostic strings, and the repair sequence.

rule sync is deprecated

rule sync no longer synchronizes anything. All it does now is migrate, offline, the rule.lock file and cache directory an earlier version left behind: it vendors each cached rulebook that still matches its recorded digest into the live path its source loads from, then deletes both. Every run opens with:
doctor reports any leftovers as the info finding Rulebook lock and cache leftovers detected. See rule sync for every message the migration prints and the case where it refuses to run.

Transparent wrappers

If your team runs commands through a wrapper such as rtk, analysis sees the wrapper by default, not the command underneath. Listing the wrapper in transparent_wrappers lets CC Safety Net look through it to the visible protected child command, so both built-in analysis and your custom rules apply to rtk git reset --hard and rtk docker system prune exactly as they would to the bare commands. Configure wrappers with the rule wrapper subcommand rather than editing rule.json by hand:
Rules for the field:
  • There are no built-in defaults. Configure only wrappers you intentionally trust.
  • A wrapper name must match ^[a-zA-Z][a-zA-Z0-9_-]*$ and must be unique within the file.
  • Reserved commands cannot be wrappers: git, busybox, the built-in analyzed commands rm, find, xargs, and parallel, every shell wrapper, every interpreter, and the awk interpreters.
  • Unwrapping finds the first protectable child command after wrapper flags and VAR=value assignments, or the token immediately after an explicit --. A child that is not itself protectable is not unwrapped.
  • A wrapper that is not listed here, or one that rewrites or hides its child command rather than exec’ing a visible child, is still not unwrapped. Only the top-level dangerous-text fallback scan may catch such a command.
transparent_wrappers lives in rule.json. If a scope’s rule.json becomes unreadable, that scope’s wrappers stop applying. This is the one place where dropped configuration reduces built-in coverage. A rulebook that fails to load leaves rule.json readable, so wrappers survive it.

Create your first custom rule

Create a starter project rule config:
This creates an inert .cc-safety-net/rules/rule.json. No rulebook sources are configured yet:
Add --example to also write an inactive example rulebook at .cc-safety-net/rules/example-rules/rulebook.json. It is written only when that file does not already exist, and rule init does not reference it, so you must add it as a source to make it active:
To author your own rulebook, create .cc-safety-net/rules/project-rules/rulebook.json and register it with npx -y cc-safety-net rule add project-rules. That leaves rule.json looking like this:
Rule definitions live in that rulebook file:
Saving the file is enough. From the next command onwards, git add -A, git add --all, and git add . are blocked with your custom message. To check the file before that, run:

rule.json schema

The top-level rule.json selects active rulebooks, applies overrides, and declares transparent wrappers. It is separate from policy.json, which configures safety levels, built-in protections, allow and deny paths, and audit retention. See Policy for that file.
integer
required
Schema version. Must be 1.
array
List of rulebook source strings. Defaults to an empty array. Source names must be unique within the file, and at most 64 sources are allowed. See Resource limits.
object
Rule overrides keyed by <rulebook-name>/<rule-name>. Values are either "off" to disable a rule, or an object to replace the rule’s block message. The object form requires reason and accepts an optional intent; an omitted intent leaves the rule’s own intent unchanged.
array
Command names that transparently execute a visible protected child command, so analysis looks through them. Defaults to an empty array. Entries must be unique and must not be reserved commands. See Transparent wrappers.
An override that changes both the message and the agent-facing intent looks like this:

rule.json editor support

CC Safety Net publishes a JSON Schema for rule.json. The file is maintained by hand, and a test fails whenever its fields, patterns, limits, or enums drift from the validator the runtime uses. Point your editor at it for completion and validation:
It covers exactly the rule.json fields above: version, rules, overrides, and transparent_wrappers. Running rule verify adds this $schema reference to a valid rules config that lacks one. There is no published schema for policy.json.

Rulebook schema

Each rulebook lives in its own rulebook.json file.
integer
required
Rulebook schema version. Must be 1 or 2. Any other value fails validation with rulebook_version must be 1 or 2. See Version 2 rules.
string
required
Rulebook name. Must match the local directory name or GitHub source name.
string
required
Rulebook version string.
string
Human-readable description of the rulebook.
string
Rulebook author.
array
required
Commands this rulebook is allowed to define rules for.
array
required
Custom blocking rules. See Rule schema.
array
Optional rulebook fixtures. See Fixture schema. Version 1 fixtures are shape-validated only. Version 2 fixtures are also evaluated against the rulebook’s own rules.

Rule schema

The fields below define a rulebook_version 1 rule. Version 2 replaces subcommand and block_args with a match object; see Version 2 rules.
string
required
Unique within the rulebook. Must start with a letter, followed by letters, numbers, hyphens, or underscores. Maximum of 64 characters.
string
required
Base command to match. Must be listed in allowed_commands.
string
Subcommand to match, for example add or install. If omitted, matches any subcommand.
array
required
Arguments that trigger the block (at least one required).
string
required
Message shown when blocked. Maximum of 256 characters.
string
Agent behavior intent appended to the block message footer. One of hard_stop, use_alternative, scope_down, manual_only, or stop_and_explain. Defaults to manual_only.

Version 2 rules

Set "rulebook_version": 2 to match on an exact command path instead of a subcommand plus a bag of arguments. A version 1 rule that blocks delete under gcloud compute also blocks gcloud compute instances create delete, because it looks for the token anywhere in the command. A version 2 rule with the command path ["compute", "instances", "delete"] does not. Version 1 rulebooks keep their fields, their matching, and their shape-only fixtures. Each rulebook is validated against the version it declares. A version 2 rule keeps name, command, reason, and intent from version 1 and replaces subcommand and block_args with a match object.
array
required
Command words that must follow the command, in order. Non-empty array of non-empty strings.
array
At least one of these tokens must appear literally among the arguments. Non-empty array of unique non-empty strings.
array
Any of these tokens appearing literally among the arguments prevents the match. Non-empty array of unique non-empty strings.
Version 2 rejects the version 1 fields instead of ignoring them. A rule that still carries subcommand or block_args fails validation with rules[0].subcommand: not supported in rulebook_version 2 and rules[0].block_args: not supported in rulebook_version 2.

Version 2 matching

  • Command: Normalized to its lowercase basename, as in version 1.
  • Command path: CC Safety Net walks the arguments and skips recognized value-taking global options together with their values. The command words it then meets must equal command_path exactly, in order. Arguments after the path do not affect the path match.
  • Global option tables: Value-taking global options are built in for aws, gcloud, and az only. Terraform needs no table. Its one global option, -chdir=DIR, is =-joined, so it is skipped as a single token.
  • Unrecognized options: A token starting with - that is not in the table for that command is skipped without consuming a value. An unlisted value-taking option written with a separate value (--newflag value) therefore makes the rule miss. The miss is deliberate. CC Safety Net fails open rather than block on an option it does not recognize, so as a rulebook author, treat this as a known gap and document it in the rulebook.
  • No short-option expansion: -Ap stays -Ap. List every spelling you want to catch, such as "-destroy" and "--destroy".
  • Literal and case-sensitive: No regex, glob, or substring matching.
  • First match wins: Rules are evaluated in order, and the first rule that matches produces the block.
  • Release channels need their own rule: gcloud beta compute instances delete does not match a command_path of ["compute", "instances", "delete"]. Write a second rule with ["beta", "compute", "instances", "delete"].

Fixture schema

Fixtures document intended behavior. CC Safety Net parses their commands and runs them through the rulebook’s rules. It never executes them.
string
required
Shell command fixture.
string
required
Either blocked or allowed.
string
Rule expected to block the command. Required for blocked fixtures.
Version 1 fixtures are shape-validated only. Version 2 fixtures are evaluated against the rulebook’s own rules, both when rule add or rule update fetches a source and when rule verify reads a rulebook directory. A blocked fixture passes only when its named rule is the first match; an allowed fixture passes only when no rule matches. A failing fixture rejects that source before the file is written, so a rulebook that contradicts its own fixtures never becomes active. Loading a rulebook does not re-evaluate them. Each failure names the fixture by its index in tests:
rule verify prefixes each one with the rulebook file, as example-rules/rulebook.json: tests[0]: ....

Matching behavior

The subcommand, argument, and option rules below describe rulebook_version 1 rules. Version 2 rules match as described in Version 2 matching. Command normalization, execution order, and transparent wrappers apply to both.
  • Command normalization: Commands are reduced to their basename before matching. /usr/local/bin/npm matches a rule with "command": "npm".
  • Subcommand detection: The subcommand is the first non-option argument following the command. In git --no-pager add -A, the subcommand is add.
  • Argument matching: Arguments in block_args are matched literally. No regex or glob support.
  • Short option expansion: Bundled short flags are unbundled before matching. -Ap is treated as -A and -p.
  • Long option matching: Long options use exact string matching. --all-files does not match --all.
  • Any-argument matching: A command is blocked if any single argument in block_args is present.
  • Additive only: Custom rules can only add new restrictions. They cannot bypass built-in protections.
Known limitation: -Cfoo is treated as -C -f -o -o, not -C foo. Blocking -f may false-positive on attached option values.

Examples

Prevent the agent from installing packages globally:
Block docker system prune:

Block message format

What a block looks like owns the full block message layout. What a custom rule adds is a prefix carrying the rulebook name and the rule name, so you can tell which rulebook produced the block:
The prefix is <rulebook-name>/<rule-name>. This is also the key you use in rule.json overrides to disable a rule ("off") or replace its reason.

Validate your rulebooks

After creating or editing rulebooks, validate them with:
rule verify checks both scopes’ rule.json, loads each configured source the way the guard loads it, and validates every rulebook directory under .cc-safety-net/rules/ in the current repository, including the version 2 fixtures. It never fetches remote content.

Migrate legacy configuration

Legacy inline config files (.safety-net.json and ~/.cc-safety-net/config.json) are no longer loaded at runtime. CC Safety Net never enforces legacy rules from their old location, and they do not block work. The runtime does not inspect legacy files, so no warning appears at guard time. Run npx -y cc-safety-net rule verify after an upgrade to find a leftover legacy file.
Before. A single inline config with embedded rules:
After. rule migrate creates a rulebook-based layout automatically:

Invalid custom-rule configuration

Custom-rule configuration that fails to load is dropped, not enforced, and never turned into a denial. Ordinary commands keep running, every other valid source keeps enforcing, and every built-in protection still applies. The runtime reports itself as degraded so the situation is visible. Because a dropped source removes denials rather than adding them, this failure produces no friction on its own. Run npx cc-safety-net status after configuration changes and upgrades. See Configuration recovery for the full failure-to-fallback matrix, the diagnostic strings, the reporting surfaces, and the repair sequence.
Custom rule configuration is not tamper-resistant. rule.json and the rulebook files are best-effort; only policy.json is a protected path. If you add or modify custom rules manually, always validate them with npx -y cc-safety-net rule verify.
Last modified on September 15, 2026