Rule configuration file locations
CC Safety Net loads rulebooks from two scopes and merges them:- User scope.
~/.cc-safety-net/rules/rule.json(created withrule init --global). Use this for personal defaults that apply to every project. - Project scope.
.cc-safety-net/rules/rule.jsonin the project root. Use this for team or project-specific rules you can commit to source control.
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
degradedstate. Rename one of the rulebooks in its rulebook file and in therule.jsonthat 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
overridesapply 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_wrappersfrom both scopes are unioned.
Managing rulebook sources
A rulebook source is referenced by an entry inrule.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.jsonin that repository and ref.rule addandrule updatevendor 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.
rule command to add, update, and remove sources rather than editing rule.json by hand:
--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-v2works.
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
Therules 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 asrtk, 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:
- 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 commandsrm,find,xargs, andparallel, every shell wrapper, every interpreter, and the awk interpreters. - Unwrapping finds the first protectable child command after wrapper flags and
VAR=valueassignments, 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.
Create your first custom rule
Create a starter project rule config:.cc-safety-net/rules/rule.json. No rulebook sources are configured yet:
--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:
.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:
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.
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:
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 ownrulebook.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 arulebook_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.
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_pathexactly, 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, andazonly. 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:
-Apstays-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 deletedoes not match acommand_pathof["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.
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 describerulebook_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/npmmatches 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 isadd. - Argument matching: Arguments in
block_argsare matched literally. No regex or glob support. - Short option expansion: Bundled short flags are unbundled before matching.
-Apis treated as-Aand-p. - Long option matching: Long options use exact string matching.
--all-filesdoes not match--all. - Any-argument matching: A command is blocked if any single argument in
block_argsis 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
Block global npm installs
Block global npm installs
Prevent the agent from installing packages globally:
Block dangerous docker commands
Block dangerous docker commands
Block
docker system prune:Multiple rules in one rulebook
Multiple rules in one rulebook
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:<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.
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 asdegraded 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.