你上周已经把 pnpm test 加进了白名单。今天 Claude Code 又停下来要权限,你点同意,再跑一次,又要权限。或者一条你以为某条已有规则该覆盖的命令(git status 显然该落在 Bash(git:*) 下)还是不断弹窗。
**最快的修法:**模式几乎肯定是少了一个末尾通配符。Bash(pnpm test) 只匹配字面串 pnpm test,不包括 pnpm test --filter auth。把它改成 Bash(pnpm test:*)(或等价的 Bash(pnpm test *)),让它匹配该命令外加任意参数。用 /permissions 命令打开规则编辑器,改掉这条,再重跑。
如果不是这个原因,那它八成是这几件具体的事之一:模式没命中模型真正发出的命令、settings 文件选错了、复合命令没通过逐子命令匹配、ask 或 deny 规则压过了 allow,或者 —— 这是 2026 年 OS 沙箱带来的新情况 —— 某条命令无法在沙箱内运行,回退到了常规权限弹窗。Claude Code 的 Bash 匹配器做的是基于模式、且懂 shell 的匹配,不是字面字符串比较,所以通配符、管道、包装命令的规则都很微妙。本文每一条规则都在 2026 年 6 月对照过官方 permissions 和 sandboxing 文档。
你属于哪一类?
| 症状 | 最可能的原因 | 跳到 |
|---|---|---|
| 每次都问,命令带额外参数 | 模式缺末尾 :* / * | 第 2 步 |
| 规则明明在,却被忽略 | settings 文件 / 作用域错了 | 原因 2 |
被拦命令含 |、&&、; | 逐子命令匹配 | 第 5 步 |
git status / ls / cat 还弹窗 | 只读集没命中,或有 ask 规则 | 原因 8 |
timeout 30 npm test 弹窗 | 包装命令没被剥离(watch、find -exec) | 原因 7 |
弹窗提到网络域名或 docker/gh | OS 沙箱回退 | 原因 9 |
| 拦截信息提到 “hook” 或退出码 2 | PreToolUse 钩子 | 原因 6 |
| 什么都对不上,所有规则都被忽略 | JSON 无效,静默回退到拒绝 | 第 3 步 |
常见原因
按”哪个最常是真正的根因”排序。
1. 白名单模式跟实际命令对不上
Claude Code 是拿模型输出的字面命令串去匹配,包括参数。Bash(pnpm test) 只精确匹配 pnpm test —— 不包括 pnpm test --filter auth,不包括 pnpm test -- --watch。要参数容忍,得加末尾通配符:Bash(pnpm test:*) 或 Bash(pnpm test *)。
**怎么判断:**权限提示里的命令比你的规则模式更长,或参数不一样。
2. settings 文件作用域错了
规则可以放在五个地方,按如下优先级求值(最高在前):managed settings(管理员下发)、命令行参数、.claude/settings.local.json(个人,gitignore)、.claude/settings.json(项目,进 git)、~/.claude/settings.json(用户/全局)。权限规则在这几处之间是合并而非完全覆盖,但任一层级的 deny 都压过任一层级的 allow。你把规则加进某份文件,却从一个看不到它的目录启动 Claude,那就等于没生效。
怎么判断:cat .claude/settings.local.json 显示规则在,但提示还是弹;或者你改了全局,但项目级的 deny 或 ask 排在它前面。
3. 复合命令是逐子命令匹配的
这一点跟旧行为不同了。Claude Code 现在懂 shell:它按 &&、||、;、|、|&、& 和换行把复合命令拆开,然后要求每个子命令都各自命中一条规则。Bash(grep:*) 不会放行 cat file.txt | grep foo —— 不是因为匹配器被管道搞糊涂了,而是因为 cat 那一半没有对应规则。你需要同时有 Bash(cat:*) 和 Bash(grep:*)。
**怎么判断:**被拦的命令里有 |、&&、;、$(...) 或反引号,而其中至少一段没被单独放行。单独的 grep foo file.txt(只读)没问题。
4. 规则放进了 ask 而非 allow
permissions.ask 列的是即便 allow 也命中仍要弹窗的模式。规则按 deny -> ask -> allow 的顺序求值,首个命中即生效,具体程度不改变这个顺序。像 Bash(git push:*) 这种内容级 ask 规则会强制弹窗,即便有更宽的 allow 也命中。测试完忘了把模式从 ask 移出来,很常见。
怎么判断:allow 和 ask 数组里都能看到该模式。从 ask 删掉就好。
5. settings 文件 JSON 无效
多一个逗号、括号不配对,整份 settings 都加载失败,Claude 回退到默认值。除非看 verbose 日志,否则没任何横幅提示。
怎么判断:jq . .claude/settings.json 报解析错误;或 claude --debug 在启动时打了 failed-to-parse 一行。
6. deny 比预期更宽
像 Bash(rm:*) 这种宽 deny 会把 rm -rf node_modules 一并拦下,即便有 allow 规则该覆盖它。deny 在每个层级都赢过 allow。注意:裸工具 deny(没括号的 Bash)会把 Bash 整个从模型上下文里移除,模型根本不会去试 —— 那是另一种”工具不可用”的症状,而不是弹窗。
**怎么判断:**命令是破坏性的(rm、mv 到 /tmp、删库),且 permissions.deny 里有匹配前缀。
7. exec 包装命令破坏前缀匹配
匹配前,Claude Code 会剥离一组固定的包装命令 —— timeout、time、nice、nohup、stdbuf,以及不带标志的 xargs —— 所以 Bash(npm test:*) 也能覆盖 timeout 30 npm test 和 xargs npm test。但另一些包装命令不会被剥离,总是弹窗:watch、setsid、ionice、flock、带标志的 xargs(xargs -n1 ...),以及带 -exec 或 -delete 的 find。环境运行器如 npx、docker exec、devbox run、mise exec 也不剥离;你得写一条同时含运行器和内层命令的规则,例如 Bash(devbox run npm test)。
**怎么判断:**被拦的命令以 watch、setsid、flock、find ... -exec、xargs -<标志> 或某个工具运行器开头,而内层命令本身是放行的。
8. 内置只读命令被覆盖了
Claude Code 在所有模式下都不弹窗就运行一组固定命令:ls、cat、echo、pwd、head、tail、grep、find、wc、which、diff、stat、du、cd,以及 git 的只读形式(所以 git status 和 git diff 本不该弹窗)。如果其中某条突然弹窗,要么你为它加了显式的 ask 或 deny 规则,要么你给一条可写命令(git、sed、sort、find)传了未加引号的通配符 —— 这会强制弹窗,因为该通配符可能展开成危险标志。
**怎么判断:**一条平时静默的只读命令弹窗了。检查 permissions.ask/deny 里有没有它,以及该命令是否带了未加引号的 * 通配符。
9. OS 沙箱无法运行该命令
这是最新的一类原因。Bash 沙箱(跑 /sandbox 查看)在操作系统层面强制文件系统和网络边界 —— macOS 上用 Seatbelt,Linux 和 WSL2 上用 bubblewrap。在**自动放行模式(auto-allow)**下,沙箱内的命令完全不弹窗就运行。但一条无法在沙箱内运行的命令会回退到常规权限流程并弹窗。常见触发:它需要一个你没预先放行的网络域名(任何新域名首次命中都会弹窗),或者它是已知与沙箱不兼容的工具 —— docker、依赖 watchman 的 jest,以及在 Seatbelt 下会 TLS 校验失败的 Go CLI(gh、gcloud、terraform)。
**怎么判断:**提示或报错提到某个主机/域名、某项沙箱限制,或上述某个特定工具,并且沙箱已启用(/sandbox 显示 auto-allow 模式生效)。
开始前
- 把权限提示里的精确命令字符串逐字节复制下来。
- 记录你最近改的是哪份 settings,以及沙箱是否开着(
/sandbox)。 - 给故障分类:“每次都问”(allow 规则缺失或对不上)、“始终被拒”(deny 规则、裸工具 deny 或钩子),还是”偶发”(常常是沙箱网络回退或某条复合命令)。
- 用
claude --version确认版本。懂 shell 的复合命令拆分、包装命令剥离、OS 沙箱都是 2026 年的行为;旧版本匹配方式不同。
需要收集的信息
- Claude 试图运行的完整命令(从提示里抓)。
- 所有相关 settings 文件:
.claude/settings.json、.claude/settings.local.json、~/.claude/settings.json,以及任何 managed settings。 - 配置的
PreToolUse钩子。 - 被拦时的
claude --debug输出。 - 该命令是否含管道、子 shell、重定向,或
timeout/watch/xargs之类的包装命令。 - OS 沙箱是否启用、处于哪种模式。
一步步修复
最便宜的检查先做。
第 1 步:看清楚被拦的命令到底是什么
看提示或 claude --debug 日志。如果 Claude 想跑:
pnpm test --filter @app/auth -- --watch
那 Bash(pnpm test) 这条规则永远不会命中。先把完整字符串抄下来,再去碰规则。
第 2 步:用末尾通配符
用 /permissions 打开规则编辑器,或直接编辑 .claude/settings.json / .claude/settings.local.json:
{
"permissions": {
"allow": [
"Bash(pnpm test:*)",
"Bash(pnpm run lint:*)",
"Bash(git diff:*)",
"Bash(node -e:*)"
]
}
}
Bash(cmd:*) 表示”该前缀后跟任意参数”。它和空格形式 Bash(cmd *) 完全等价 —— 你在提示里点”Yes, don’t ask again”时,Claude Code 自己写下的就是空格形式。有两个值得知道的坑:
:*形式只在模式末尾才被识别。Bash(git:* push)里的冒号会被当成字面字符,这条规则不会命中。- 空格形式带词边界:
Bash(ls *)命中ls -la但不命中lsof,而Bash(ls*)(无空格)两者都命中。
第 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
规则按 deny -> ask -> allow 求值,首个命中即生效。模式若在 ask,即便 allow 也命中仍会弹窗。前缀若在 deny(任一作用域,包括 managed settings),无论如何都被拦。决定它该在哪份名单里,从其他列表移除。
第 5 步:把复合命令的每个子命令都放行
因为 Claude Code 按 shell 操作符拆开、逐段匹配,一条管道命令需要每一段都被放行。要放行 cat config.json | grep token,把两半都放行:
{
"permissions": {
"allow": [
"Bash(cat:*)",
"Bash(grep:*)"
]
}
}
不要为了”支持管道”去用 Bash(*grep*) 这种模糊通配符 —— 现在既没必要,又危险(它还会命中 rm -rf foo && grep)。在提示里用”Yes, don’t ask again”放行一条复合命令时,Claude 会按子命令分别存规则(最多 5 条),这正是你想要的。像 Bash(curl https://github.com/ *) 这种约束参数的模式很脆弱;要做网络过滤,就 deny 掉 curl/wget,改用 WebFetch 工具加 WebFetch(domain:github.com),或者用 OS 沙箱来强制(第 7 步)。
第 6 步:审计钩子
PreToolUse 钩子在权限规则之前运行。如果它以退出码 2 结束,就会在任何 allow 规则被检查之前拦掉这次调用。
ls .claude/hooks/
cat .claude/hooks/pre-tool-use.sh
确认脚本对合法命令返回 0。加日志,下次被拦时可查:
echo "$(date) PreToolUse: $CLAUDE_TOOL_NAME $CLAUDE_TOOL_INPUT" >> .claude/hook.log
第 7 步:在 OS 沙箱和 bypass 模式之间做选择
如果你在密集迭代、想让大多数命令不弹窗就跑,启用 OS 级 Bash 沙箱,而不是放宽白名单。跑 /sandbox,选 auto-allow 模式,把额外的可写路径或域名显式授予:
{
"sandbox": {
"enabled": true,
"filesystem": {
"allowWrite": ["~/.kube", "/tmp/build"]
},
"network": {
"allowedDomains": ["registry.npmjs.org", "github.com"]
}
}
}
在 auto-allow 模式下,沙箱内的命令静默运行;只有内容级 ask 规则、显式 deny 规则,以及针对 / 或主目录的 rm/rmdir 仍会弹窗。与沙箱不兼容的工具(docker、Go CLI、watchman)要加进 excludedCommands,或用 jest --no-watchman 这类标志。完整键参考见官方指南:Claude Code sandboxing 文档。
如果你只是想在一次性 worktree 或容器里彻底跳过弹窗:
claude --dangerously-skip-permissions
它等同于 --permission-mode bypassPermissions。以 root 或 sudo 运行时会被拦下,除非处在被识别的沙箱里;而 rm -rf / 或 rm -rf ~ 仍会触发断路保护弹窗。绝不要在有个人数据的宿主机上用;配合容器或 git worktree,把影响半径限定住。相关审批流问题见 Claude Code 权限提示循环。
如何确认已修复
- 重跑之前被拦的命令,确认现在能直接通过、不再弹窗。
- 换不同参数再跑(
pnpm test --filter foo),验证末尾通配符的容忍工作正常。 - 跑一条你没有放行的命令,确认仍然弹窗 —— 证明你没把所有东西都放开。
- 看
claude --debug输出,确认是哪条规则命中。 - 如果启用了沙箱,看
/sandbox的 Config 选项卡,确认解析出的文件系统和网络边界跟你预期一致。
长期预防
.claude/settings.json作为进 git 的团队文件,.claude/settings.local.json留给个人覆盖(Claude Code 会自动把本地文件加进.gitignore)。- 多子命令合理的工具用前缀模式(
Bash(pnpm:*)),破坏性动词保持显式。 - 把白名单当作最小权限清单 —— 只加真正需要的,别预防性地放开裸
Bash。 - 在 CI 里校验 JSON;一份坏掉的 settings 不应静默回退到拒绝。
- 自动化或高信任工作流,优先用 OS 沙箱而非宽白名单 —— 即便有提示注入绕过了模型判断,它仍在 OS 层强制边界。
- 把白名单策略写进
CLAUDE.md,这样模型输出的命令本身就贴合规则;参见 Claude Code 项目 CLAUDE.md 未加载。
常见坑
- 为了消掉一次提示加了
Bash(rm:*),几周后误删丢了文件。 - 从文档里粘贴带智能引号的命令 ——
pnpm "test"跟pnpm test不是同一条。 - 改了
~/.claude/settings.json,但项目级deny或某个 managed setting 排在它前面。 - 为支持管道去用模糊的
Bash(*grep*),而其实放行每个子命令更安全。 ask和allow混了 ——ask是”提醒我”,allow是”直接放行别问”。- 以为包装命令是透明的:
watch npm test和find . -exec ... \;即便npm test已放行也总会弹窗。 - 指望 Bash 沙箱阻止模型干蠢事,而不是在
CLAUDE.md写清楚规则;参见 Claude Code 权限提示循环。
FAQ
Q:我加了 Bash(git:*) 但 git commit 还是弹窗,为什么?
git commit 不在只读集里,所以需要一条 allow 规则 —— 而 Bash(git:*) 本该覆盖它。如果还弹窗,检查是否有像 Bash(git commit:*) 这种内容级 ask 规则优先(ask 压过 allow),或者某份 settings 文件解析失败了。跑 jq . .claude/settings.json,并用 /permissions 看每条规则来自哪份文件。
Q:为什么 Bash(echo:*) 命中不了 echo "hello" | tee file.txt?
因为 Claude Code 按管道拆开、逐段匹配。echo 被覆盖了,但 tee 没有,所以这次调用弹窗。再加一条 Bash(tee:*),或者用”Yes, don’t ask again”放行一次这条复合命令,它会按子命令存规则。
Q:为什么 npm test 已放行,timeout 30 npm test 还弹窗?
timeout 在被剥离的包装命令列表里,所以这条本该能跑。如果还弹窗,你多半撞上的是不被剥离的包装命令(watch、flock、setsid、xargs -n1 或 find -exec)。这些总会弹窗;为完整命令串写一条精确匹配规则,或直接跑内层命令。
Q:CI 里能不能一键全部放行?
可以 —— claude --dangerously-skip-permissions(等同 --permission-mode bypassPermissions)就是为无人值守的封闭环境准备的。以 root 运行时它会被拦下,除非处在被识别的沙箱里,所以用 dev container 配置或非 root 用户。配一个干净的临时文件系统使用。
Q:白名单管不管模型跑的脚本内部的命令?
不管。白名单卡的是顶层 Bash 调用。shell 脚本一旦跑起来,操作系统允许什么它就能做什么 —— 除非启用了 OS 沙箱,那它会在每个子进程上强制文件系统和网络边界。Bash(./scripts/*) 这种规则要谨慎。
Q:一条命令明明在我的白名单里,却就某个网络域名弹窗,这是怎么回事?
那是 OS 沙箱,不是权限白名单。默认没有任何域名被预先放行;任何命令首次访问一个新主机时,沙箱都会弹窗。把它加进 sandbox.network.allowedDomains(或弹窗时同意),之后的运行就静默了。
Q:钩子在求值顺序里处于什么位置?
PreToolUse 钩子在权限规则之前运行。钩子以退出码 2 结束就直接拦掉调用,在任何 allow 规则被检查之前。deny 和 ask 规则无论钩子返回什么仍然生效,所以一条匹配的 deny 会拦掉即便被放行的调用。PostToolUse 在工具返回结果之后运行。
标签: #Claude Code #bash #sandbox #权限 #排查