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 acceptsGIT_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.
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 analyzesdd, 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:
.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
rmof a protected directory. mvwhen a protected file or directory is a source.
[, cat, file, grep, head, jq, less, ls, more, rg, sed, stat, tail, test, wc.
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:
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 likebash -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, setCC_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, orgh issue create.
$, a backtick, or a backslash denies with:
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.
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.