你上周已经把 pnpm test 加进了项目白名单。今天 Claude Code 又停下来要权限,你点同意,再跑一次,又要权限。更糟的情况是:一条你以为某条已有规则该覆盖的命令(git status 显然该被 Bash(git:*) 命中)还是不断弹窗。Claude Code 的 Bash 沙箱做的是基于模式的命令匹配,不是字面字符串匹配,引号、管道、子 shell 和前缀匹配的细节都很微妙。绝大多数”沙箱拦了我以为该放行的命令”问题,最后都归结为:模式没能命中模型真正发出的命令、settings 文件作用域不对,或是 allow 和 ask 搞混了。
常见原因
按”哪个最常是真正的根因”排序。
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.json 的 allow 和 ask 数组里都能看到该模式。从 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。 - 配置的
PreToolUse或PostToolUse钩子。 - 被拦时的
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(*)没事,然后在公司机器上打开同一目录。 ask和allow混了 ——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 #权限 #排查