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 for a guided first run and Installation for per-agent setup.
Command overview
The CLI registers twelve 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 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.
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.
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 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.
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: <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.
Managed hook drift
Cursor and Grok Build keep their hook entry in a config file you can also edit by hand, sodoctor 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 (<Agent>): <message>, and the exit code does not change. Rerunning the install rewrites the entry.
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 <path>: <reason>Error (Cursor): Failed to parse Cursor hooks config <path>: <reason>Error (Grok Build): Failed to parse Grok Build hooks config <path>: <reason>
<Agent> inspection failed finding with error severity, so doctor exits 1.
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). Stdout and the exit code stay unchanged.
Machine-readable output
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
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 either Would remove <n> legacy audit log files (<size>). 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 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. Two things make it exit 1. The first is option validation:
- 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.
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:
-- 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.
See the Explain trace reference 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.
Running rule with no subcommand prints help and exits 1. rule --help prints the same help and exits 0.
Options:
--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 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 preservesrules, overrides, and transparent_wrappers. It creates no cache directory.
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.
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 isrule add [source] [--ref <ref>] [--only <rulebook...>]. 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/<rulebook-name>.
Options:
Examples:
--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 <rulebook...> 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/<name>/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: <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: <ref>.
rule.json stores the canonical owner/repo#ref/<rulebook-name> carrying the ref you gave. rule add reports the resolved commit but does not store it.
A successful add opens with Scope: project (<config dir>), or Scope: user (<config dir>) 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 <n> rulebooks from <source> at <ref>:(rulebookwhen it is one), then one- <name>line per rulebookRulebooks already configured from <source> at <ref>: <names>for the selected rulebooksrule.jsonalready listedVendored at <commit>., the resolved commit abbreviated to 7 characters, printed only when the add wrote at least one new source- one block per file written:
Vendored <spec> (<version>)for a new file, orUpdated <spec> (<before> -> <after>)followed by+ <rule>,- <rule>, and~ <rule>lines for rules added, removed, and changed under an unchanged name Rule config updated., a blank line, thenActive rulebooks (<n>):with- <name> <version> (<n> rules)andSource: <spec>for each
owner/repo#ref/<rulebook-name> source keeps the scope line, drops the first three entries above, and closes with Added rulebook source: <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:
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: <dir>. 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-recursivermdir. A file that lands after the unlink makes thermdirfail, 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: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, 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 <spec>: <message>, 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
<config-dir>/<name>/rulebook.json and reported as Vendored <spec> from the v2 cache., or as Restored <spec> 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 <spec> from the v2 cache. Run `cc-safety-net rule update <spec>` to vendor it., with --global appended to that command in the user scope. The last line is Removed the v2 lock and cache under <dir>.
With no leftovers to migrate, the command prints No v2 lock or cache leftovers found in <dir>; 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 <dir> 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: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 <command> <subcommand> on Command: when it sets a subcommand, and its blocked arguments on Block args:. A rulebook_version: 2 rule puts <command> 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:
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.- 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 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
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 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.
policy
policy checks and applies policy proposals. The fields a policy file carries, and what each one does, are documented in Policy.
Options:
Examples:
.cc-safety-net/policy.json in the project, or your user policy file under --global.
Output
Both subcommands print the same report beforeapply writes anything:
Scope: user (<path>) 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: <arg>, Unknown policy subcommand: <name>, policy <subcommand> requires a file, and Unexpected policy argument: <arg>. A project proposal carrying an audit section is rejected the same way, because audit settings are user scope only:
Applying
apply requires a TTY on both stdin and stdout. Without one it prints the command for you to run and exits 1:
--global when you passed it.
In a terminal, apply asks Apply this policy to <path>? [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: <path>, also exiting 0. A project apply writes only the fields the proposal sets; every field it leaves out keeps inheriting from your user policy.
The guard denies an agent that runs policy apply, with intent hard_stop and this reason:
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 for per-agent steps, post-install actions, and legacy plugin-identifier migration.
Targets
Thirteen 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 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 andamp 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.tsbecause it would mask the personal plugin. After a change, restart Amp or runplugins: reload. For Hermes Agent, installation writes the plugin to disk and runshermes plugins enable. Restart Hermes after a change.
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 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, 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 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 <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 a new integration and leaves unconfigured agents unchanged.
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 <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, 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-<uid>-<package>@<version-or-latest>, 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:
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-<digits>-. 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: <flag>, while a positional argument prints Unexpected argument for update: <arg>. 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.
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.
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 for how each agent plugs in.
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 for its views and confirmation behavior.
Options
--no-open is the only argument gui accepts. Any other argument prints an error: Unknown option for gui: <arg> for an option or Unexpected argument for gui: <arg> 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: <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 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.
🛡️ 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.