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

Claude Code 反复对你已加白名单的命令弹窗。修法:模式末尾加通配符、拆开复合命令、检查新的 OS 沙箱回退。2026 年 6 月核对。

你上周已经把 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 文件选错了、复合命令没通过逐子命令匹配、askdeny 规则压过了 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 弹窗包装命令没被剥离(watchfind -exec)原因 7
弹窗提到网络域名或 docker/ghOS 沙箱回退原因 9
拦截信息提到 “hook” 或退出码 2PreToolUse 钩子原因 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 显示规则在,但提示还是弹;或者你改了全局,但项目级的 denyask 排在它前面。

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 移出来,很常见。

怎么判断:allowask 数组里都能看到该模式。从 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 整个从模型上下文里移除,模型根本不会去试 —— 那是另一种”工具不可用”的症状,而不是弹窗。

**怎么判断:**命令是破坏性的(rmmv/tmp、删库),且 permissions.deny 里有匹配前缀。

7. exec 包装命令破坏前缀匹配

匹配前,Claude Code 会剥离一组固定的包装命令 —— timeouttimenicenohupstdbuf,以及不带标志的 xargs —— 所以 Bash(npm test:*) 也能覆盖 timeout 30 npm testxargs npm test。但另一些包装命令不会被剥离,总是弹窗:watchsetsidioniceflock、带标志的 xargs(xargs -n1 ...),以及带 -exec-deletefind。环境运行器如 npxdocker execdevbox runmise exec 也不剥离;你得写一条同时含运行器和内层命令的规则,例如 Bash(devbox run npm test)

**怎么判断:**被拦的命令以 watchsetsidflockfind ... -execxargs -<标志> 或某个工具运行器开头,而内层命令本身是放行的。

8. 内置只读命令被覆盖了

Claude Code 在所有模式下都不弹窗就运行一组固定命令:lscatechopwdheadtailgrepfindwcwhichdiffstatducd,以及 git 的只读形式(所以 git statusgit diff 本不该弹窗)。如果其中某条突然弹窗,要么你为它加了显式的 askdeny 规则,要么你给一条可写命令(gitsedsortfind)传了未加引号的通配符 —— 这会强制弹窗,因为该通配符可能展开成危险标志。

**怎么判断:**一条平时静默的只读命令弹窗了。检查 permissions.ask/deny 里有没有它,以及该命令是否带了未加引号的 * 通配符。

9. OS 沙箱无法运行该命令

这是最新的一类原因。Bash 沙箱(跑 /sandbox 查看)在操作系统层面强制文件系统和网络边界 —— macOS 上用 Seatbelt,Linux 和 WSL2 上用 bubblewrap。在**自动放行模式(auto-allow)**下,沙箱内的命令完全不弹窗就运行。但一条无法在沙箱内运行的命令会回退到常规权限流程并弹窗。常见触发:它需要一个你没预先放行的网络域名(任何新域名首次命中都会弹窗),或者它是已知与沙箱不兼容的工具 —— docker、依赖 watchmanjest,以及在 Seatbelt 下会 TLS 校验失败的 Go CLI(ghgcloudterraform)。

**怎么判断:**提示或报错提到某个主机/域名、某项沙箱限制,或上述某个特定工具,并且沙箱已启用(/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*),而其实放行每个子命令更安全。
  • askallow 混了 —— ask 是”提醒我”,allow 是”直接放行别问”。
  • 以为包装命令是透明的:watch npm testfind . -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 在被剥离的包装命令列表里,所以这条本该能跑。如果还弹窗,你多半撞上的是不被剥离的包装命令(watchflocksetsidxargs -n1find -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 #权限 #排查