Claude Code Bash 沙箱阻止预期命令 —— 排查与修复

Claude Code 拒绝运行你已加白名单的命令,Bash 沙箱判定它不安全或未授权。从权限作用域、模式匹配、settings.json 配置三方面定位。

你上周已经把 pnpm test 加进了项目白名单。今天 Claude Code 又停下来要权限,你点同意,再跑一次,又要权限。更糟的情况是:一条你以为某条已有规则该覆盖的命令(git status 显然该被 Bash(git:*) 命中)还是不断弹窗。Claude Code 的 Bash 沙箱做的是基于模式的命令匹配,不是字面字符串匹配,引号、管道、子 shell 和前缀匹配的细节都很微妙。绝大多数”沙箱拦了我以为该放行的命令”问题,最后都归结为:模式没能命中模型真正发出的命令、settings 文件作用域不对,或是 allowask 搞混了。

常见原因

按”哪个最常是真正的根因”排序。

1. 白名单模式跟实际命令对不上

Claude Code 是拿模型输出的字面命令串去匹配,包括参数。Bash(pnpm test) 只能精确匹配 pnpm test —— 不包括 pnpm test --filter auth,不包括 pnpm test -- --watch。要参数容忍,得写 Bash(pnpm test:*)

怎么判断:权限提示里的命令比你的规则模式更长、参数不一样。

2. settings 文件作用域错了

.claude/settings.json 在项目根,.claude/settings.local.json 是个人覆盖,~/.claude/settings.json 是全局。你把规则加进其中一份,但 Claude 启动的目录看不到它,等于没生效。

怎么判断:cat .claude/settings.local.json 显示规则在,但提示还是弹;或者你改了全局,但项目级有另一份(更小的)白名单覆盖了。

3. 管道和子 shell 破坏模式匹配

Bash(grep:*) 命中 grep foo file.txt,但匹配不到 cat file.txt | grep foo(因为开头命令是 cat,不是 grep)。组合命令要么写更宽的规则,要么拆成多次调用。

怎么判断:被拦的命令里有 |&&;$(...) 或反引号。单独的 grep foo file.txt 没问题。

4. 规则放进了 ask 而非 allow

permissions.ask 列的是即便有 allow 命中也要弹窗的模式。若一条规则同时匹配两个数组,更严的(通常是 ask)生效。测试完忘了从 ask 移出来,很常见。

怎么判断:settings.jsonallowask 数组里都能看到该模式。从 ask 删掉就好。

5. settings 文件 JSON 无效

多一个逗号、括号不配对,整份 settings 都加载失败,Claude 回退到默认拒绝。除非看 verbose 日志,用户那边没任何提示。

怎么判断:cat .claude/settings.json | jq . 报错;或者 claude --debug 在启动时打了 failed to parse settings

6. deny 比预期更宽

Bash(rm:*) 这种宽 deny 会把 rm -rf node_modules 一并拦下,即便有 allow 规则。deny 永远赢过 allow。

怎么判断:命令是破坏性的(rm、mv 到 /tmp、删库)。permissions.deny 数组里有匹配前缀。

7. 钩子在沙箱之前就拦了

settings 里的 PreToolUse 钩子可以在白名单检查之前就拒掉任何 Bash 调用。钩子脚本只要返回非零,这次调用就被拒。

怎么判断:拦截信息提到 “hook” 或非零退出码,而不是 “permission denied”。

开始前

  • 把权限提示或拒绝信息里出现的”精确命令字符串”逐字节复制下来。
  • 记录你最近改的是哪份 settings。
  • 判断是”每次都问”(allow 规则缺失或匹配不上)、“始终被拒”(deny 规则或钩子),还是”偶发”。
  • 确认 Claude Code 版本(claude --version);各版本规则引擎一直在收紧。

需要收集的信息

  • Claude 试图运行的完整命令(从提示里抓)。
  • 三份 settings 文件:.claude/settings.json.claude/settings.local.json~/.claude/settings.json
  • 配置的 PreToolUsePostToolUse 钩子。
  • 被拦时的 claude --debug 日志。
  • 该命令是否含管道、子 shell 或重定向。

一步步修复

最便宜的检查先做。

第 1 步:看清楚被拦的命令到底是什么

看提示或日志。如果 Claude 想跑:

pnpm test --filter @app/auth -- --watch

那你的规则 Bash(pnpm test) 永远不会命中。先把完整字符串抄下来,再去调规则。

第 2 步:用 :* 做前缀匹配

.claude/settings.json.claude/settings.local.json:

{
  "permissions": {
    "allow": [
      "Bash(pnpm test:*)",
      "Bash(pnpm run lint:*)",
      "Bash(git status:*)",
      "Bash(git diff:*)",
      "Bash(node -e:*)"
    ]
  }
}

:* 表示”该前缀后跟任意参数”。不加它,你只能匹配光秃秃的命令。

第 3 步:校验 JSON

jq . .claude/settings.json
jq . .claude/settings.local.json
jq . ~/.claude/settings.json

每个都应输出可解析的 JSON。报错就意味着 Claude 静默回退到默认值 —— 先把语法修好。

第 4 步:检查 ask / deny 冲突

jq '.permissions' .claude/settings.json

模式若在 ask,即便 allow 也命中仍会弹窗。前缀若在 deny,无论如何都被拦。决定它该在哪份名单里,从其他列表移除。

第 5 步:拆开复合命令

与其为管道命令写一条巨型规则,不如让 Claude 分步执行:

Run `grep -r "TODO" src/` (allowed). Save the output to a temp variable. Then run `wc -l` on it.

如果真要支持管道,显式加一条宽规则:

"Bash(*grep*)"

要清楚这条很宽。破坏性动词上避免通配符规则。

第 6 步:审计钩子

如果钩子才是把关人:

ls .claude/hooks/
cat .claude/hooks/pre-tool-use.sh

确认脚本对合法命令返回 0。加日志:

echo "$(date) PreToolUse: $CLAUDE_TOOL_NAME $CLAUDE_TOOL_INPUT" >> .claude/hook.log

下次被拦时可查。

第 7 步:在封闭沙箱里用 --dangerously-skip-permissions

当你在一个一次性 worktree 或容器里迭代、对工作区完全信任时:

claude --dangerously-skip-permissions

绝不要在有个人数据的宿主机上用。配合 worktree 把影响半径限定住;相关审批流问题见 Claude Code 权限提示循环

验证

  • 重跑之前被拦的命令,确认现在能直接通过、不再弹窗。
  • 换不同参数再跑(pnpm test --filter foo),验证 :* 前缀容忍工作正常。
  • 跑一条白名单外的命令,确认仍然弹窗 —— 证明你没把所有东西都放开。
  • claude --debug 输出,确认是哪条规则命中。

长期预防

  • .claude/settings.json 作为团队文件(进 git),.claude/settings.local.json 留给个人覆盖(gitignore 掉)。
  • 多子命令合理的工具用前缀模式(Bash(pnpm:*)),破坏性动词保持显式。
  • 把白名单当作最小权限清单 —— 只加真正需要的,别预防性地放开 Bash(*)
  • 在 CI 里校验 JSON;一份坏掉的 settings 不应该静默回退到拒绝。
  • 把白名单策略写进 CLAUDE.md,这样模型输出的命令本身就贴合规则;参见 Claude Code 项目 CLAUDE.md 未加载
  • 定期清理不用的规则 —— 它们只增加表面积、不带价值。

常见坑

  • 为了消掉一次提示加了 Bash(rm:*),几周后误删丢了文件。
  • 从文档里粘贴带智能引号的命令 —— pnpm "test"pnpm test 不是同一条。
  • 改了 ~/.claude/settings.json,但启动的项目有更严的本地文件。
  • 假定个人仓库 Bash(*) 没事,然后在公司机器上打开同一目录。
  • askallow 混了 —— ask 是”我想被提醒”,allow 是”直接放行别问”。
  • 指望 Bash 沙箱阻止模型干蠢事,而不是在 CLAUDE.md 写清楚规则;参见 Claude Code 权限提示循环

FAQ

Q: 我加了 Bash(git:*)git commit 还是弹窗,为什么?

可能 Bash(git commit:*)ask 规则优先,或者 settings 文件 JSON 解析失败。跑 jq . .claude/settings.json 看看。

Q: CI 环境里能不能一键全部放行?

可以 —— --dangerously-skip-permissions 就是为无人值守的封闭环境准备的。配一个干净的临时文件系统使用。

Q: 白名单管不管模型跑的脚本内部的命令?

不管。白名单卡的是顶层 Bash 调用。shell 脚本一旦跑起来,操作系统允许什么它就能做什么。Bash(./scripts/*) 这种规则要谨慎。

Q: 为什么 Bash(echo:*) 命中不了 echo "hello" | tee file.txt?

因为整条命令带管道,匹配器看的是整个表达式。第一条命令是 echo,但模式看到的是完整管道形式。要么拆开,要么改用更宽的模式。

Q: 钩子在求值顺序里处于什么位置?

PreToolUse 钩子在权限检查之前运行。钩子返回非零就直接拦截调用,不管白名单。PostToolUse 在工具返回结果之后运行。

标签: #Claude Code #bash #sandbox #权限 #排查