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 incc-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:
~/.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.
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:
--idcannot be combined with--agent,--rule,--session,--project,--suspect,--since, or--limit.--prune-legacycannot be combined with--id,--agent,--rule,--session,--project,--suspect,--all,--since, or--limit.--jsonand--dry-runare 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
Nested per-project audit logs are never touched, and the command says so afterwards. It exits0 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.
-- 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:
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>;--cwdwithout a value prints--cwd requires a value. Either parse error is followed byUsage: cc-safety-net explain [--json] [--cwd <path>] <command>andPass -- before a command that starts with dashes., and exits1. - A
--cwdpath that does not exist printsError: --cwd path does not exist: <path>and exits1. - An empty command prints
Error: No command providedplus the usage line and exits1.
-- 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.
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 preservesrules, 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:
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>:
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 editingrule.json by hand:
--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, orlist. wrapper listtakes no further argument. It printsTransparent wrappers: (none)or a numbered list.wrapper addandwrapper removeeach 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. addde-duplicates;removefilters. Scope follows-g/--global.
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: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
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: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:
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 andamp login; the published plugin applies to every Amp session, including Orb threads. A leftover managed local copy at~/.config/amp/plugins/cc-safety-net.tswould mask the personal plugin, so install removes it. After a change, restart Amp or runplugins: reload. For Hermes Agent, the install writes the plugin to disk and also runshermes plugins enable, and a change needs a Hermes restart.
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 showsalready installed, and one that is not configured showsnot installedduring uninstall. On Windows, the probes — like the installs themselves — resolve npm.cmdshims through the shell, so a CLI installed with npm is detected instead of showingCLI 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.
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.
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.
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.
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.
🛡️ 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.
help <command> or <command> --help to see usage for a specific command:
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.