工具大全
踩坑实录作者:Coocon2026年10月21日208 次阅读约 7 分钟阅读

Claude Code 禁止 rm -rf 怎么配:deny 规则两周拦下 14 次,实测还有 7 种写法照样能删,一个 hook 补齐

问题背景

Claude Code 跑着跑着,一条命令被挡下,模型收到的是这样一行:

Permission to use Bash with command cd /tmp && rm -rf smc2 && mkdir smc2 && cd smc2 && unzip -q ... has been denied.

没有理由,没有出处。如果你和我一样,曾经在 ~/.claude/settings.json 里亲手写过这么一条,那它就是你自己加的:

{
  "permissions": {
    "allow": ["Bash"],
    "deny": ["Bash(rm -rf *)"]
  }
}

加它的目的很朴素:让 Claude Code 随便跑命令,但别让它一个 rm -rf 把东西删没了。这篇回答三个问题:这条规则实际拦下了什么,它漏了什么,怎么把漏洞补上又不把模型卡死。

结论先说

  • 这条 deny 规则只认字面上的 rm -rf 。rm -fr、rm -r -f、rm -Rf、rm -r 在三种权限模式下都能删掉目录。
  • **模型很守规矩。**本机两周 14 次被拒,模型没有一次换写法绕过去,代价是留下一堆 xxx2 目录和没删掉的旧产物。
  • **还有一道你没配过的闸。**Claude Code 内置的 dangerous-rm 检查会拦 $D/* 这类「变量为空就变成删根目录」的写法,bypass 模式也拦,交互模式下会停下来等人批准。
  • **要补齐,用 PreToolUse hook。**它在三种模式下都先于其他规则生效,还能把「为什么不让删、该怎么办」直接告诉模型。

两周的真实记录:拦下 14 次,0 次绕过

我翻了本机 10 月 7 日到 21 日 5 个项目的 Claude Code 会话记录,找出所有带 rm 且被拒的工具调用,再看模型下一步干了什么。

被拒的命令(节选) 模型下一步
rm -rf $S && git checkout-index -a --prefix=$S/ 换成 snap2/ 重来
rm -rf .venv && uv venv -p 3.11 .venv 新建 .venv311
cd /tmp && rm -rf smc2 && mkdir smc2 && unzip … 解压到项目 tmp 下的新目录
rm -rf wtt-film && … pipeline.py init wtt-film 改用 wtt-film2
rm -rf $D; mariadb-install-db --datadir=$D 换成 tmp/mariadb
rm -rf tmp/lt/patchtest && mkdir -p tmp/lt/patchtest … 目录名加上时间戳
rm -rf /tmp/pc && mkdir -p /tmp/pc … 改用 /tmp/pc2
cd /tmp && rm -rf hyperframes && git clone … clone 到另一个位置
rm -rf node_modules package-lock.json; npm i … 不删了,先看当前状态
rm -rf dial.mp4.frames sheet.png … 跳过删除,继续后面的活

完整统计:

模型反应 次数
换一个新目录名或新位置重来 8
放弃删除,只做命令里的其他部分 6
换成 rm -fr / find -delete / /bin/rm 等写法绕过 0

我又在这些会话的全部 Bash 调用里搜了 rm -fr、find -delete、/bin/rm -r、shutil.rmtree、git clean -f,一条都没有。

这说明主人的意图被尊重了。但有两个代价:

  1. **整条复合命令都不执行。**被拒的命令大多是「删旧目录 + 重建 + 跑测试」连在一起的,rm 只是其中一段,结果后面的构建和测试也一起没跑,模型要多花一轮拆开重发。
  2. 垃圾目录越积越多。snap 和 snap2、.venv 和 .venv311、wtt-film 和 wtt-film2 并排躺着。被拒文案里没有任何指引,模型只能自己想办法,最省事的办法就是换个名字。

实验:22 种删除写法,逐字回放

光看真实记录只能说明「模型没去绕」,说明不了「规则能不能被绕」。我在 /tmp 下搭了一个隔离实验:

  • **模型由本地 stub 扮演。**它把指定的 Bash 命令原样作为 tool_use 发给 Claude Code,再把 Claude Code 回给模型的 tool_result 原文记下来。客户端行为是真实的,模型回复是模拟的。
  • **每次全新环境。**空的 CLAUDE_CONFIG_DIR、假 API key、外连一律拦截,删的是每次新建的沙箱目录 s/,里面放 a/x.txt、a/sub/y.txt、b.txt、t_1.jpg、t_2.jpg。
  • **用磁盘结果判对错。**跑完列出沙箱剩下什么,不看模型说了什么。
  • **三种权限模式。**同一份 settings(allow Bash + deny Bash(rm -rf *)),分别用手动模式(--permission-mode default)、auto 模式、bypass 模式(--dangerously-skip-permissions)跑。

先说一个意外:在 2.1.285 上,不指定权限模式时 sonnet 5.5 的会话默认就是 auto。本机真实会话也大多跑在 auto 下。

结果:deny 规则 Bash(rm -rf *)

写法 手动模式 auto 模式 bypass
rm -rf s/a 拦 拦 拦
rm -fr s/a 删了 删了 删了
rm -r -f s/a 删了 删了 删了
rm -Rf s/a 删了 删了 删了
rm -r s/a 删了 删了 删了
/bin/rm -rf s/a 删了 交给分类器 删了
find s/a -delete 删了 交给分类器 删了
bash -c 'rm -rf s/a' 删了 交给分类器 删了
cd s && rm -rf a && echo after 拦(整条) 拦(整条) 拦(整条)
echo before; rm -rf s/a; echo after 拦(整条) 拦(整条) 拦(整条)
D=s/a; rm -rf $D 拦 拦 拦
rm -rf "$(pwd)/a" 拦 拦 拦
trash s/a 执行 交给分类器 执行
mv s/a s/.trash-a 执行 执行 执行

「交给分类器」那几格,实验里没有真实分类器,结果是 temporarily unavailable, so auto mode cannot determine the safety of Bash。真实环境下分类器会怎么判,这次没测。

几点解读:

  • **规则是按命令文本匹配的。**它能拆开 &&、; 找到其中的 rm -rf,也能穿过前面的 D=… 赋值,但 -rf 换个顺序就不认了。官方权限文档写得很直白:Bash(rm *) 拦得住 rm -rf build/,拦不住 /bin/rm -rf build/ 和 bash -c 'rm -rf build/',deny 规则「不是安全边界」。
  • 被拒就是整条不跑。echo before; rm -rf …; echo after 里的 echo before 也没执行。
  • **auto 模式会扔掉宽泛的 allow。**调试日志里有一行 Ignoring dangerous permission Bash(*) from ../settings.json (bypasses classifier)。所以 auto 下 /bin/rm、find -delete 没有被直接放行,而是送去分类器。
  • **auto 模式下,工作目录里的 rm -fr 连分类器都不过。**调试日志写的是 Skipping auto mode classifier for Bash: would be allowed in acceptEdits mode。删的是工作目录里的东西,就被当成和改文件同一级别的操作。

第二道闸:内置的 dangerous-rm 检查

除了你自己写的 deny 规则,Claude Code 还内置了一道删除检查。本机真实触发过一次,命令是先用 heredoc 改一个文件,再清空临时目录、重新出图:

python3 - <<'EOF'
...改 film.js...
EOF
D=tmp/jj; rm -f $D/*; node core/render/still.mjs ... --out $D ...

模型收到的原文(节选):

Permission for this command was denied by a built-in Claude Code safety check, not by the user.
... Only a person may approve such a removal, and no person did (the permission prompt timed out,
or this session cannot prompt). The command was NOT run; do not claim it succeeded. Do not work
around the check by splitting, scripting, or re-issuing the removal through another tool or shell
... What was flagged: Dangerous rm operation detected in `rm -f $D/*`. The target '$D/*' is a shell
variable expansion: when $D is unset or empty it becomes `/`, `/*` or a top-level path.

This check does not fire on a target that cannot expand to the filesystem root: rewrite it as
`"${D:?}"/*`, which makes the shell stop with an error instead of running rm when $D is unset or
empty, or use a literal absolute path.

那次是交互式 auto 会话,工具调用发出后等了 120 秒没人批准,才拿到这条拒绝。前面改文件的 heredoc 也一起没执行。

我在实验里复现了它,并找到了触发边界:

写法 手动模式 auto 模式 bypass
D=s/a; rm -f $D/*(单行) 执行 交给分类器 执行
前面有一段 heredoc,再 D=a; rm -f $D/* 拦 拦 拦
D=s; rm -f $D/*(s 就是工作目录) 拦 拦 拦
D=s; rm -f "${D:?}"/* 执行 交给分类器 执行
D=s; rm -f $D/t_*.jpg 执行 交给分类器 执行
  • **单行命令里,它能跟踪前面的变量赋值。**前面多了一段 heredoc,就跟不上了,只能按「变量可能为空」处理。
  • **目标是工作目录本身时,换一种文案拦。**原文是 This command would remove a workspace directory (the working directory, an additional working directory, or one of their parent directories). This requires explicit approval and cannot be auto-allowed by permission rules.
  • bypass 模式也拦。--dangerously-skip-permissions 跳不过它。
  • **照提示改成 "${D:?}"/* 就能过。**变量为空时 shell 直接报错退出,不会执行 rm。通配符前带上字面前缀($D/t_*.jpg)也不触发。

这道检查在最近的版本里一直在收紧。changelog 里能看到:2.1.277 开始在提示里点名被拦的命令并建议 ${VAR:?},2.1.281 把「变量 + 顶层目录名」「从工作目录推导的变量」也纳入,2.1.304 把 /Volumes、/media 这类挂载点顶层也纳入。我测的是 2.1.285,更新的版本行为可能更严。

补缝:用 PreToolUse hook 拦住所有递归删除

deny 规则只能写文本前缀,hook 可以跑任意逻辑。下面这个脚本把常见的递归删除写法都认出来,拒绝时附上理由和出路:

#!/usr/bin/env python3
# ~/.claude/hooks/no-recursive-rm.py
# PreToolUse hook:拦截各种写法的递归删除,并告诉模型该怎么做
import json, re, sys

cmd = json.load(sys.stdin).get("tool_input", {}).get("command", "")
patterns = [
    # rm -rf / -fr / -r -f / -R / /bin/rm -rf ……
    r"(^|[\s;&|(`'\"])(\S*/)?rm\s+(-[a-zA-Z]*[rR][a-zA-Z]*\s+|-[a-zA-Z]+\s+)*-[a-zA-Z]*[rR]",
    r"(^|[\s;&|(`'\"])(\S*/)?rm\s+--recursive",
    r"\bfind\b.*\s-delete\b",
    r"\bgit\b.*\bclean\b.*-[a-zA-Z]*[fdx]",
]
if any(re.search(p, cmd) for p in patterns):
    print(json.dumps({"hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "permissionDecision": "deny",
        "permissionDecisionReason": (
            "本机主人禁止 Claude 递归删除(rm -r / find -delete / git clean)。不要换写法重试。"
            "需要干净目录就换一个新目录名(如 out_2);确实要清掉旧东西,用 trash <路径> 移进废纸篓,"
            "或者在回复里列出要删的路径,由主人自己删。")}}, ensure_ascii=False))

挂到全局 settings,原来那条 deny 规则留着当第二层:

{
  "permissions": {
    "deny": ["Bash(rm -rf *)"]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "~/.claude/hooks/no-recursive-rm.py" }]
      }
    ]
  }
}

记得 chmod +x。把前面 deny 规则那一轮的 19 种写法重跑一遍:

写法 手动模式 auto 模式 bypass
rm -rf / rm -fr / rm -r -f / rm -Rf / rm -r 拦 拦 拦
/bin/rm -rf / find -delete / bash -c 'rm -rf …' 拦 拦 拦
git clean -fdx 拦 未测 拦
复合命令、变量、$(pwd) 里的 rm -rf 拦 未测 拦
rm -f s/b.txt 等非递归删除 执行 未测 执行
trash s/a、mv s/a … 执行 未测 执行

auto 模式只抽测了 rm -fr、/bin/rm -rf、bash -c 三条,都在 hook 这一步被拒,没有走到分类器。

脚本本身另外拿 17 条常见命令测过:docker run --rm、docker rm -f、git rm --cached、grep -rn "rm"、rm -f a.txt、git clean -n 都放行;sudo rm -fr、xargs rm -r、find -exec rm -rf 都拦。会误伤的有两种:git rm -r --cached dist,以及字符串里出现的 echo "do not rm -rf"。前者偶尔会用到,被拦时让模型改成逐个文件 git rm --cached 或者交给你执行。

模型这回收到的是:

PreToolUse:Bash hook error: 本机主人禁止 Claude 递归删除(rm -r / find -delete / git clean)。不要换写法重试。需要干净目录就换一个新目录名(如 out_2);确实要清掉旧东西,用 trash <路径> 移进废纸篓,或者在回复里列出要删的路径,由主人自己删。

前缀写着 hook error,但这就是正常的拒绝,不是 hook 出错。和原来那句光秃秃的 has been denied 比,模型知道了这是主人的规矩,也知道了三条出路。

实验日志还显示,每一次被拒的调用,PreToolUse hook 都先执行了,PostToolUse 没有执行。本机 hook 日志里的真实拒绝也是这个顺序。所以 hook 看得到所有被拒的命令,拿来记账也合适。

它仍然不是安全边界

hook 和 deny 规则都是看命令文本。python3 -c "import shutil; shutil.rmtree('x')"、perl -e、写个脚本再执行,这些都识别不了。它们防的是模型顺手一个 rm -rf 的习惯动作,防不住刻意构造的删除。

真正兜底的还是这几样:

  • **重要目录在 git 里。**未提交的改动随手 commit 或 stash。
  • **有备份。**Time Machine 或者别的定时备份。
  • **需要硬隔离就开 sandbox。**官方文档对文件系统和网络的强制隔离推荐的是 sandboxing,不依赖命令文本。

实验中踩的坑

  • **2.1.285 不指定模式时就是 auto。**第一轮我以为在测手动模式,结果日志里全是 auto 模式的文案,只好显式加 --permission-mode default 重跑一轮。
  • **stub 找不到 tool_result,陷入死循环。**新版请求的消息列表末尾可能是一条 role: system,tool_result 不在最后一条消息里。要扫描全部消息。
  • **trash 不受实验隔离约束。**它按 macOS 的规则把文件移进当前用户的废纸篓,不认 CLAUDE_CONFIG_DIR。终端没有读取废纸篓的权限,我没能直接确认那几个沙箱目录落在哪里,只确认了它们已经从沙箱里移走。

未验证清单:auto 模式下真实分类器对 /bin/rm -rf、find -delete、bash -c、trash 会怎么判,这次没有接分类器;交互模式下内置检查的等待时长只有真实会话里的一次观测(120 秒),没在实验里测;hook 方案在 auto 模式下只抽测了 3 条;2.1.285 之后的版本没测。

相关阅读


2026-10-21 实测,Claude Code 2.1.285,macOS 27.0.1。权限规则行为依据官方权限文档与本机实验;版本变化依据 Claude Code CHANGELOG。真实会话统计来自本机 10 月 7 日至 21 日的 Claude Code 会话记录。

这类实测,每周六汇总一封

订阅码农早餐:每天 8:00 一封 AI 编程早报,每周六另附本周 Claude Code / Codex / 本地模型的实测和踩坑汇总。

相关文章

Claude Code 接 DeepSeek / GLM / Kimi 开 auto 模式:temporarily unavailable 括号里的原因怎么看,哪些重试没用(故障注入实测)

用第三方模型跑 Claude Code auto 模式,每个有副作用的动作都会用你的会话模型向同一个后端多发一次判定请求:系统提示词 132,470 字符,单次约 2.97 万输入 token,比主循环那一步还多。判定请求一失败,就出现「xxx is temporarily unavailable, so auto mode cannot determine the safety of Bash」。本文用故障注入代理把每种失败对应的报错原文测了一遍:500 → (server error)、429 → (rate-limited)、529 → (overloaded)、断连 → (connection failed),这几种会自动重试 5 次;超过 60 秒没回 → (timed out);**没有括号 = 后端拒收(4xx),只试 1 次,重试多少遍都一样**;后端回了但模型没给判定 → could not evaluate,连续 3 次就退回人工确认。另外实测 CLAUDE_CODE_AUTO_MODE_MODEL 不生效,判定器只能跟着会话模型换。Claude Code 2.1.285,真实后端 DeepSeek v4-pro 与小米 MiMo v2.5。

claude-codedeepseek+7
pitfalls2026年10月11日11 min
643

Claude Code 上下文太长怎么办:Prompt is too long 会自动压缩,中转站 / DeepSeek 的 maximum context length 却不会(实测)

Claude Code 靠报错文案判断「上下文太长」:后端说 prompt is too long 或 input is too long for requested model,它会自动压缩对话后重发,用户无感;DeepSeek 和 OpenAI 风格中转返回的 This model's maximum context length is …,它不认识,直接报 API Error: 400,每轮都失败。手动 /compact 有效;更好的办法是用 CLAUDE_CODE_MAX_CONTEXT_TOKENS(非 claude- 模型 ID)或 CLAUDE_CODE_AUTO_COMPACT_WINDOW(claude- 模型 ID)告诉它真实上限,让它提前压缩。Claude Code 2.1.285 + 本地 stub 实测。

claude-codedeepseek+6
pitfalls2026年10月5日8 min
726

Claude Code 一直卡住、转圈没反应怎么办:后端不回时它要等 6 分钟,重试满 10 次可能卡一个多小时(实测)

Claude Code 转圈不动、或者停在 Retrying in 0s,多半是后端没有回数据。实测 2.1.285(API key + ANTHROPIC_BASE_URL):后端不回响应头时每次等 6 分钟(360 秒)才判超时,把 API_TIMEOUT_MS 调到 60 万、90 万也不变,只能调小;默认重试 10 次,总共可能卡一个多小时。中转站如果把回复缓冲到生成完才返回,超过 6 分钟的回复永远拿不到。按 Esc 可以随时中断。全部在本地 stub 上实测。

claude-code中转站+6
pitfalls2026年10月5日10 min
649

Claude Code 429 怎么办:Request rejected (429) 原文、会重试多久、retry-after 超过 60 秒直接放弃(实测)

Claude Code 遇到 429 会先自动重试,最多 10 次、约 3 分钟。retry-after 不超过 60 秒时会等满再试(实测 10 秒、60 秒都恢复成功),61 秒及以上一次都不重试,立刻报 API Error: Request rejected (429)。中转站(new-api)的中文限流提示会原样显示,空 body 显示 status code (no body)。交互模式下每句话会同时发 2 个请求,限流额度消耗也是 2 倍。全部在本地 stub 上实测,Claude Code 2.1.285。

claude-code中转站+5
pitfalls2026年10月4日9 min
515