Skip to main content
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 itself does not need to be installed globally — npx/bunx fetch it on demand to run the commands documented here. This page is the command surface reference — commands, subcommands, options, and exit behavior. It is not a tutorial: for a guided first run, see the Quickstart; for per-agent setup, see Installation.

Command overview

The CLI registers eleven commands. This is the order they appear in cc-safety-net --help. 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.

Verdicts

The headline verdict is one of two values: 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:
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 reads as disabled rather than 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. 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 first when applicable, then the snapshot diagnostics — followed by 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.
Options: 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 <repo>/.github/copilot/settings.local.json, <repo>/.github/copilot/settings.json, <repo>/.claude/settings.local.json, <repo>/.claude/settings.json, $COPILOT_HOME/settings.json, then $COPILOT_HOME/config.json (COPILOT_HOME defaults to ~/.copilot). It also scans <repo>/.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 with a nonzero code when it detects a failure. Failures include no configured agent, a failed hook inspection, a failed self-test, or invalid user or project configuration. When some audit log files cannot be read, the Recent Activity section ends with Warning: <n> 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.

logs

logs reads back the audit log: one record per allowed-or-blocked command decision.
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

--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: <arg> and exits 1. When an audit log file cannot be read or a record is malformed, logs prints one warning to stderr — warning: <n> audit log sources could not be read; these results are incomplete (source when it is one) — and leaves stdout and the exit code untouched.

Machine-readable output

Human output prints one row per entry — id, timestamp, decision, agent, rule id, and the command truncated to 50 characters, with marking a segment that differs from the full command. --id instead prints a labeled detail block covering every field on the record.

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 — add --dry-run first when you want to see what would go. Age and content are irrelevant — membership is decided by file position alone.
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 Would remove <n> legacy audit log files (<size>). — or No legacy audit log files found. — then Nested v2 audit logs are not included., and, when there is something to delete, 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 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.
Options: -- 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:
After successful option parsing, explain exits 0 for blocked and allowed results. Read the result field instead of the exit code. Only option validation fails:
  • An unknown option prints Unknown option for explain: <arg>; --cwd without a value prints --cwd requires a value. Either parse error is followed by Usage: cc-safety-net explain [--json] [--cwd <path>] <command> 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: <path> and exits 1.
  • An empty command prints Error: No command provided plus the usage line and exits 1.
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 before pasting a trace into an issue or a chat.
For the JSON schema returned by --json — the ExplainResult fields and every TraceStep variant — see the Explain trace reference.

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. Running rule with no subcommand prints help and exits 1. rule --help prints the same help and exits 0. Options:

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. The command creates the rulebook cache directory if needed.
rule init on its own writes an inert configuration that contains no rules. Pass --example to also write a starter rulebook named example-rules:
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.

rule add

Add a rulebook source and sync. <source> is a bare local name (for example project-rules) or a GitHub source in the form owner/repo#ref/<rulebook-name>:
Omitting the source is an error.

rule remove

Remove a rulebook source and sync. Add --delete-source to also delete the local source directory when it is clean:

rule update

Refresh the lock and cache for the configured rulebook sources, or for a single source when one is given:
rule update with no source argument is exactly rule sync.

rule sync

Rebuild the lock and cache for every configured rulebook source. Run it after editing rule.json by hand:
With --check, both update and sync report Rule config checked. instead of Rule config synced. and leave lock and cache state alone.

rule list

List the active rulebooks and their resolved sources across both user and project scope:
rule list reads both scopes at once, so --global is rejected. It exits 1 only on policy errors; warnings are printed but exit 0.

rule wrapper

Manage transparent command wrappers — commands that pass their arguments through to another command, so CC Safety Net should analyze what is inside rather than the wrapper itself.
  • 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 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:
Exits 0 when everything is valid, non-zero when it is not. rule verify is not a pure check — 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: it inserts
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:
--cleanup deletes the legacy files after the migrated rules verify. migrate rejects --global, --check, 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:
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:
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.

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. For per-agent steps, post-install actions, and legacy plugin-identifier migration, see Installation.

Targets

Twelve targets are accepted, listed here in the order they are installed:

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 shells out to the agent’s own plugin manager and cleans up superseded plugin ids along the way. OpenClaw’s install also verifies afterwards that OpenClaw reports the plugin as loaded, then asks you to restart the OpenClaw Gateway. OpenCode’s install imports the cached package entry and verifies that it exports a callable CCSafetyNetPlugin; the install fails if OpenCode would load nothing and fail open.
  • Config-file writes — Antigravity CLI, Cursor, and Kimi Code. These three are the only agent configs CC Safety Net edits directly.
  • Managed plugin artifacts — Amp Code and Hermes Agent. Amp’s plugin is published to your account’s hosted Amp Personal Plugins repository through the amp CLI (the preflight is amp plugins repositories --json), so the install needs the amp CLI and amp login; the published plugin applies to every Amp session, including Orb threads. A leftover managed local copy at ~/.config/amp/plugins/cc-safety-net.ts would mask the personal plugin, so install removes it. After a change, restart Amp or run plugins: reload. For Hermes Agent, the install writes the plugin to disk and also runs hermes plugins enable, and a change needs a Hermes restart.
Before a config-file install (Antigravity CLI, Cursor, or Kimi Code) or a Hermes Agent install writes anything, it clears stale copies of cc-safety-net from the npx cache — every entry under the npm cache’s _npx directory ($npm_config_cache if set, otherwise ~/.npm on macOS and Linux, %LOCALAPPDATA%\npm-cache on Windows) whose node_modules contains cc-safety-net is removed. Those four integrations run the hook through npx, so this makes the freshly installed hook resolve 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: install the global hook now, or print the steps for the native Kimi plugin instead (inside Kimi Code, run /plugins install https://github.com/kenryu42/cc-safety-net — the trust prompt defaults to cancel — then /reload or 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. Because the prompt is the only path to the plugin steps, the Kimi Code picker row stays selectable during install even when the global hook is already configured — it is then labeled (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:
Space toggles the highlighted target, Enter confirms (with nothing selected it only rings the terminal bell), and Up/Down — or k/j — move between selectable rows. Pressing u (or U) during install leaves the selector and runs the update flow instead; 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 instead raises SIGINT, so the process ends the way 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 <name> integration… or Uninstalling <name> 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 anything new — an agent you have not set up is left alone.
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. Targets are found by reading each agent’s config and state files: an integration qualifies when it is detected as installed, even when it is currently disabled. Amp Code qualifies through amp plugins list output — fetched, like codex plugin list, with a 30-second timeout, because a cold run refreshes checkouts over the network and can outlast the 5-second default. GitHub Copilot CLI is the exception — it qualifies only when a CC Safety Net plugin checkout exists in Copilot’s installed-plugins directory, because its disabled state is indistinguishable from a kill-switch with nothing installed, and update must never turn into an install. Installations still on the pre-rename safety-net@cc-marketplace plugin id — in Claude Code, in Codex, or in GitHub Copilot CLI as the plugin checkout cc-marketplace/safety-net — are also picked up: updating migrates them to the current id and removes the legacy copy best-effort, so a failed legacy removal warns instead of failing the target. All targets run the same operation as install concurrently behind one Updating <n> integration… or Updating <n> 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 <Agent> not found; skipped (for example Codex not found; skipped) and the run continues. The config-file targets — Antigravity CLI, Cursor, 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, Hermes Agent, or Kimi Code. If that clear fails, only those cache-dependent targets fail; the other targets still run. When nothing qualifies, update prints No installed integrations found. Run `cc-safety-net install` to set one up. and exits 0. update takes no target flags and no arguments — only -h/--help. Any other option prints Unknown option for update: <flag>, a positional prints Unexpected argument for update: <arg>, and both exit 1. One target’s failure does not stop the run: its error prints with the same hints as install, update continues with the remaining targets, and the command exits 1 at the end when any target failed — otherwise 0. You can also reach the update flow from the interactive install selector by pressing u.

uninstall

uninstall accepts the same twelve target flags and uses the same selection rules and target order as install.
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 flags — or more than one — prints hook requires exactly one integration flag. Try: cc-safety-net hook --kimi-code, shows the command help, and exits 1. Amp Code, Codex, OpenClaw, OpenCode, and Pi have no hook flag of their own. Amp Code, OpenClaw, OpenCode, and Pi load CC Safety Net in process as a plugin or extension; Codex’s plugin invokes the shared hook --coding-cli entry point above. See 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 and uninstall commands.

Antigravity CLI entry point

install --agy-cli writes the command npx -y cc-safety-net hook --agy-cli into ~/.gemini/config/hooks.json — 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 the file when it is absent, re-enables a disabled managed entry, or appends a new one; 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.

gui

gui starts the local policy editor and opens it in your browser. For dashboard views and confirmation behavior, see Dashboard.

Options

--no-open is the only argument gui accepts. Any other argument prints an error — Unknown option for gui: <arg> for an option, Unexpected argument for gui: <arg> for a positional — then Usage: cc-safety-net gui [--no-open], and exits 1. The server always starts first and the URL is always printed as CC Safety Net policy GUI: <url>, 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:<port>/?token=<token>. Every request must carry that token, and writes must additionally 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.
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 <stdin> | <status>. statusline and 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 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.
Use help <command> or <command> --help to see usage for a specific command:
An unrecognized command prints Unknown command: <name> — or Unknown option: <name> when it starts with - — followed by Run 'cc-safety-net --help' for usage., and exits 1. help <name> for an unknown command prints Unknown command: <name> 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.
Last modified on August 18, 2026