Skip to main content
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 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. Every rule id, and how to turn a non-catastrophic rule off, is in the Destructive-command rules reference. See Modes for how to activate each level. This page is the behavior matrix for what is blocked and where each boundary falls. See Architecture for the ordered guard flow, Analysis engine for classifier logic, and Allowed commands for safe variants.

What each safety level adds

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. 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 and GIT_SSH to run an arbitrary program during network operations, and GIT_SSH_VARIANT to change how that program is invoked. The rule git.ssh-env blocks a command that sets any of these variables, or core.sshCommand through -c or GIT_CONFIG_*, and runs a network subcommand (clone, fetch, pull, push, ls-remote, submodule): Only an override set on the command line counts. A GIT_SSH_COMMAND exported by your shell profile before the agent started is your own choice and does not block git fetch, git pull, or git push. The rule is not catastrophic, so it can be turned off like any other built-in rule. See Allow a built-in rule.

Filesystem commands

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 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 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 or paranoid. 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. 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. 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:
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. The cd tracking follows shell scope. A cd inside a subshell, a command substitution, or a process substitution applies only until that shell ends. A cd in a brace group or a called function body applies to the rest of the command. cd - returns to the directory the last cd left. 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:
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. 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 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.

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. 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. 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, and blocks the rest. 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.

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 and paranoid. An unquoted delimiter whose body contains $, a backtick, or a backslash denies with:
Every other unsupported form or consumer denies with:
The full heredoc contract, including how heredoc content is handled in standard mode, is on the 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. 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 <arg>. 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 for its flags, and Explain trace for the JSON schema.
Last modified on September 16, 2026