Skip to main content
A custom rule blocks a command that the built-in rules allow, such as git push or npm install -g, with a reason you write. Rules live in rulebook files, and a rule.json in each scope lists the rulebooks to load. Custom rules only add blocks. They never relax a built-in rule.
Legacy inline configuration files (.safety-net.json and ~/.cc-safety-net/config.json) no longer load. Their rules do nothing until you migrate them.
The /cc-safety-net skill can write rules for you. Describe what you want, for example /cc-safety-net block terraform destroy in this project.

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.
If neither file exists, only built-in rules apply. In the directory that holds your user .cc-safety-net/, usually your home directory, the project path is the user config, so rule subcommands that write the project scope refuse to run there. See rule in the user config directory.

Scope merge behavior

  • CC Safety Net loads the user scope first and combines both scopes’ rulebooks.
  • When both scopes list a rulebook with the same name, the user rulebook wins and the project one contributes no rules. CC Safety Net reports a warning and the runtime shows degraded. Rename one of them to fix it.
  • Each scope’s overrides apply only to its own rules, so project config cannot disable or rewrite a user rule. CC Safety Net ignores, with a warning, a project override that names a user rule and any override key that matches no rule.
  • CC Safety Net combines transparent_wrappers and command_tools from both scopes.

Managing rulebook sources

Each entry in rule.json’s rules array is a rulebook source: Use the rule command instead of editing rule.json by hand:
Add --global (-g) to change the user scope instead of the project scope. Official rulebooks explains --only and --ref, and rule covers every subcommand, its output, and its errors.

Rulebooks are live files

The runtime reads each rulebook.json from disk on every tool call, so a saved edit applies to the next command. A missing, unreadable, or invalid rulebook contributes no rules, and the runtime reports degraded. For a GitHub source you have never fetched, run npx -y cc-safety-net rule update. See Configuration recovery.

Transparent wrappers

A transparent wrapper is a command that runs another command, such as timeout 5 git push. CC Safety Net looks through a transparent wrapper to the command it runs, so built-in rules and your custom rules apply to timeout 5 docker system prune as they do to docker system prune. Eight wrappers are built in and need no configuration: caffeinate, exec, nice, nohup, setsid, stdbuf, time, and timeout. If your team runs commands through another wrapper, such as rtk, add it:
  • A wrapper name must match ^[a-zA-Z][a-zA-Z0-9_-]*$ and be unique within the file.
  • Reserved commands cannot be wrappers: git, busybox, rm, find, xargs, parallel, every shell wrapper, every interpreter, and the awk interpreters.
  • Add only a wrapper that runs its child command as written.
The wrappers you add live in rule.json, so an unreadable rule.json drops them. See Coverage declared in rule.json.

Community shell tools

A plugin can give an agent its own shell tool. The dsh-ops plugin, for example, adds ops_bash and ops_run_background. CC Safety Net does not know those names, so it checks their path arguments but never reads their command as a shell command. Until you declare them, ops_bash can run git reset --hard unblocked. Declare the tool and CC Safety Net analyzes its commands:
That writes the command_tools array:
From the next tool call, a declared call gets the same built-in rules, custom rules, and safety level as a built-in shell tool. git.reset-hard then blocks ops_bash running git reset --hard.
  • The name must match the tool name the agent sends exactly, including case. rule tool add refuses built-in tool names. See rule tool add name errors.
  • cwd names the argument that holds the working directory. Without it, CC Safety Net reads workdir, cwd, or working_directory, then falls back to the session workspace.
  • A blank working directory denies the call as tool-call.malformed, and a directory that does not exist denies it as cwd.requested-unusable.
Declarations live in rule.json, so an unreadable rule.json drops that scope’s tools. Run npx -y cc-safety-net rule verify after editing the file by hand.

Create your first custom rule

1

Create rule.json

This creates an empty .cc-safety-net/rules/rule.json with no rulebook sources:
rule init --example also writes an example rulebook at .cc-safety-net/rules/example-rules/rulebook.json. It stays inactive until you run npx -y cc-safety-net rule add example-rules.
2

Write a rulebook

Create .cc-safety-net/rules/project-rules/rulebook.json:
3

Register it

rule.json now lists "rules": ["project-rules"]. From the next command, CC Safety Net blocks git add -A, git add --all, and git add . with your reason.
4

Validate it

rule verify checks both scopes’ rule.json, loads each source the way the runtime does, and runs the version 2 fixtures. It never fetches remote content.

rule.json schema

rule.json selects active rulebooks, applies overrides, and declares transparent wrappers and community shell tools. Safety levels, built-in protections, allow and deny paths, and audit retention live in a separate file. See Policy.
integer
required
The schema version, which must be 1.
array
List of rulebook source strings. Defaults to an empty array. Source names must be unique within the file, and the file can list at most 64 sources.
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
Extra commands to look through, on top of the eight built-in wrappers. Defaults to an empty array. Entries must be unique and must not be reserved commands. See Transparent wrappers.
array
Plugin shell tools whose command CC Safety Net analyzes like a built-in shell tool’s. Each entry is a tool name, or an object like { "name": "ops_bash", "cwd": "dir" }. Defaults to an empty array. Names must be unique and must not be built-in tool names. See Community shell tools.
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. Point your editor at it for completion and validation:
rule verify adds this $schema line to a valid rule.json that lacks one. There is no published schema for policy.json.

Rulebook schema

Each rulebook lives in its own rulebook.json file.
integer
required
The rulebook schema version, which must be 1 or 2. Any other value fails validation with rulebook_version must be 1 or 2. See Version 2 rules.
string
required
The rulebook name, which 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
The commands this rulebook can define rules for.
array
required
Custom blocking rules. See Rule schema.
array
Optional rulebook fixtures. See Fixture schema.

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
A name unique within the rulebook, at most 64 characters long. It must start with a letter, followed by letters, numbers, hyphens, or underscores.
string
required
The base command to match. allowed_commands must list it.
string
The subcommand to match, for example add or install. Without it, the rule matches any subcommand.
array
required
Arguments that trigger the block. At least one is required, so a version 1 rule cannot block every use of a subcommand. Use a version 2 rule for that.
string
required
The message CC Safety Net shows when the rule blocks a command, at most 256 characters long.
string
The intent for the agent, 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 set 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. 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, as a non-empty array of non-empty strings. Because the path cannot be empty, no rule matches every call of a command, such as every aws invocation.
array
At least one of these tokens must appear literally among the arguments. It is a non-empty array of unique non-empty strings. Leave it out to match the command path with any arguments.
array
If any of these tokens appears literally among the arguments, the rule does not match. It is a 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.

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.
CC Safety Net checks only the shape of version 1 fixtures. rule add, rule update, and rule verify run version 2 fixtures. A blocked fixture passes when its named rule is the first match, and an allowed fixture passes when no rule matches. rule add and rule update reject a source whose fixtures fail, before writing it. 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

Both versions match tokens literally and case-sensitively, with no regex, glob, or substring matching. --all-files does not match --all. Rules run in order, and the first match produces the block. A rule applies to nested commands too. Under a rule for rm, bash -c 'rm -r ./tmpdir', eval 'rm -r ./tmpdir', and a called function body all block, including for commands a built-in analyzer also covers.

Version 2 global options

CC Safety Net knows the value-taking global options of aws, gcloud, and az only. For any other command, an option with a separate value, such as git -C repo push, makes the rule miss. Terraform’s -chdir=DIR is one token, so it is skipped. Release channels need their own rule. gcloud beta compute instances delete does not match ["compute", "instances", "delete"]. Write a second rule with ["beta", "compute", "instances", "delete"].
CC Safety Net treats -Cfoo as -C -f -o -o, not -C foo. A rule that blocks -f can therefore match an attached option value by mistake.

Rules match commands, not file paths

A custom rule matches a shell command and its arguments. It does not see the files an agent opens with its own read and edit tools, so a rule cannot protect a file such as ~/.zshrc. To block every access to a file or folder, add it to secret_protection.deny_paths in policy.json. A deny path also blocks reads.

Examples

Block every git push

A version 2 rule with command_path: ["push"] and no any_args blocks git push with any arguments. A version 1 rule cannot do this, because block_args needs at least one argument.
The rule also blocks timeout 5 git push, because timeout is a transparent wrapper. It misses git -C repo push. See Version 2 global options.

Turn off one rule

To turn off a custom rule, add its <rulebook-name>/<rule-name> key to overrides in the rule.json of the scope that lists the rulebook, with the value "off". The block message prints this key.
The rulebook file stays unchanged, so rule update never undoes the override. To turn off a built-in rule such as git.push-force, use policy.json instead. See Allow a blocked command.

Block global npm installs

Block message format

What a block looks like describes the full block message layout. A custom rule adds the rulebook name and the rule name as a prefix, so you can tell which rulebook produced the block:
The prefix <rulebook-name>/<rule-name> is also the key you use in rule.json overrides to turn the rule off or replace its reason.

Migrate legacy configuration

CC Safety Net no longer loads legacy inline config files (.safety-net.json and ~/.cc-safety-net/config.json). Their rules do nothing, and the runtime gives no warning. npx -y cc-safety-net doctor and rule verify report a leftover file as Legacy inline rule configs are ignored.
rule migrate has no --global and migrates both scopes. From your home directory it skips the project half. See rule migrate in the user config directory. It turns the inline files into this layout:

Invalid custom-rule configuration

CC Safety Net drops custom-rule configuration that fails to load instead of denying commands. Other sources and built-in rules keep applying, and the runtime reports degraded. Nothing alerts you to the lost blocks, so run npx -y cc-safety-net status after editing rules or upgrading. See Configuration recovery.
Custom rule configuration is not tamper-resistant. An agent can edit rule.json and the rulebook files. Only policy.json is a protected path. If you add or change custom rules by hand, validate them with npx -y cc-safety-net rule verify.
Last modified on October 27, 2026