Claude Code 同一条命令反复问权限

明明给过 allow,Claude Code 还在反复弹同一个 Bash/Edit 提示。多半是 allow pattern 太窄、放错了 settings 范围,或者命令是复合命令——这里给出精确解法。

你刚给一个 Bash(npm test) 点过 “Yes, and don’t ask again”,两分钟后同样的提示又跳出来。你往 allowlist 加了一条,下一个 Bash(npm run build) 还是在问。每次 Edit、每次 shell 命令都要点一下——你以为的”agent”成了一个 session 要点一百次确认的钥匙保管员。

最快解法: 在 Claude Code 里跑 /permissions,看清当前加载了哪些规则、各来自哪个文件,确认你的命令是否真的命中了某条规则。如果没命中,截至 2026 年 6 月最常见的原因是:命令是复合命令(Claude Code 会按 &&|; 拆分,每个子命令都要有自己的规则)、pattern 太具体没法匹配末尾的 flag、或者规则放在了这个项目根本不读的 settings 范围里。如果只是想在一个信任的项目里让提示彻底消停,按 Shift+Tab 切到更宽松的权限模式,或者设 "defaultMode": "acceptEdits"。下面把这些一句话解法拆成一套可靠流程。

先搞懂权限系统怎么运作

每一次弹窗都由三件事决定:

  1. 三个规则列表。 settings 文件里的 permissions.allowpermissions.askpermissions.deny。求值顺序是 deny → ask → allow,第一条命中的说了算;更具体的 allow 规则压不过已命中的 ask 或 deny 规则。
  2. 权限模式。 default 对每个工具首次使用时弹窗;acceptEdits 自动接受编辑和常见文件系统命令;plan 只读;bypassPermissions 几乎全跳过。按 Shift+Tab 循环切换,或用 settings 里的 "defaultMode" 固定一个。
  3. settings 优先级。 托管设置 > 命令行参数 > .claude/settings.local.json > .claude/settings.json > ~/.claude/settings.json。任意一层的 deny,低层的 allow 都覆盖不了。

/permissions 能看到合并后的实时规则集,以及每条规则的来源文件。光这一条命令就能解决大部分”为什么还在问”的困惑。

你属于哪种情况

症状最可能的原因跳转
循环的命令里有 &&|; 或子 shell复合命令——每个子命令各需一条规则原因 1
规则写的是 Bash(npm test),命令却是 npm test --watchpattern 太窄(末尾没通配符)原因 2
规则存在,但 /permissions 里没列出来settings 范围错了或 JSON 非法原因 3
Edit(/Users/you/...) 永远不匹配路径锚点错了——那是项目根相对路径,不是绝对路径原因 4
给过 allow,hook 还是强制弹窗PreToolUse hook 返回 ask 或 exit 2原因 5
settings 里给过 allow,每次跑还在问模式是 default,你一直点 “Just this time”原因 6

常见原因和修法

原因 1:命令是复合命令(最容易踩的坑)

Claude Code 是懂 shell 的。它会按 &&||;||&& 和换行拆分命令,然后要求每个子命令都各自命中一条规则。所以 Bash(cd packages/web && npm test) 并不被单独的 Bash(npm test*) 覆盖,老式写法 Bash(cd * && npm *)授权不了整条复合命令。

修法: 给每个子命令各加一条规则。当你用 “Yes, and don’t ask again” 批准一条复合命令时,Claude Code 其实已经替你这么做了——它会按子命令分别保存规则(最多 5 条),比如批准 git status && npm test 会保存一条 npm test 的规则。手动预先写的话:

{
  "permissions": {
    "allow": [
      "Bash(npm test:*)",
      "Bash(npm run build:*)"
    ]
  }
}

裸的 cd 进入工作目录的子目录会被当成只读、不需要单独规则。有一个例外:把 cdgit 放在同一条复合命令里,不管目标目录是哪个都会弹窗。

原因 2:allow pattern 比命令更具体

Bash(npm test)精确匹配。一旦 Claude 跑 npm test --watchnpm test src/,这条规则就错过了。

用末尾通配符覆盖同族命令。两种等价写法:

  • Bash(npm test *)——注意 * 前面有个空格。这个空格强制词边界:Bash(ls *) 匹配 ls -la 但不匹配 lsof
  • Bash(npm test:*)——:* 后缀和末尾的 * 等价,也是权限弹窗倾向于写出的形式。

通配符可以放在任意位置:Bash(git * main) 同时匹配 git push origin maingit merge main。单个 * 能跨多个参数(含空格)。

进程包装器会被自动剥离,所以 Bash(npm test *) 也覆盖 timeout 30 npm test。被剥离的集合是固定的:timeouttimenicenohupstdbuf,以及不带 flag 的 xargs环境运行器不会被剥离——npxdocker execmise execdirenv execdevbox run 会把参数当命令执行,所以要写一条同时点名运行器和内部命令的规则,比如 Bash(docker exec mycontainer npm test)

原因 3:规则在这个项目不读的范围里(或 JSON 写坏了)

有三个可写层级,按用途选:

规则适用范围改哪个文件
跨所有项目的个人默认~/.claude/settings.json
团队通过 git 共享仓库根的 .claude/settings.json
这个项目里个人用、不进 git.claude/settings.local.json

npm test 这种项目特定但个人用的命令,放 settings.local.json,规则就不会跑进同事的环境。

怎么确认:/permissions。如果里面没列出你的规则,那 Claude Code 就没读到它——要么放错了范围,要么 JSON 非法(一个多余的逗号就能让整个文件静默失效)。用 python3 -m json.tool < .claude/settings.local.json 校验。当前版本会实时重读 settings,但如果你用的是旧版本,改完后重启 Claude Code。

原因 4:Edit/Read 规则的路径锚点写错了

这个几乎人人都中招。ReadEdit 规则走 gitignore 语义,有四种锚点写法,而单个前导斜杠不是绝对路径:

Pattern锚点示例
//path绝对路径,从文件系统根算起Edit(//Users/you/proj/src/**)
~/path家目录Read(~/.zshrc)
/path项目根,不是文件系统根Edit(/src/**)
path./path当前目录Read(*.env)

所以 Edit(/Users/you/proj/src/**) 会被解释成 <project-root>/Users/you/proj/src/**,什么都匹配不到。项目内相对路径用 Edit(/src/**),真正的绝对路径用 Edit(//Users/you/proj/src/**)(双斜杠)。裸文件名比如 Read(.env) 在任意深度都匹配,等同于 Read(**/.env)

原因 5:PreToolUse hook 在强制弹窗

PreToolUse hook 能返回 ask 决策,或者以 exit code 2 阻断,这两者都优先于你的 allow 规则。再厉害的 allowlist 也压不住。

把它找出来:

grep -rn "PreToolUse" ~/.claude/settings.json .claude/settings.json .claude/settings.local.json

如果有个 matcher: "Bash" 的 hook 在跑交互检查,它会无视 allow 规则、对每个 Bash 都重新提示。把 matcher 收窄到你真正在意的破坏性命令,或者直接删掉这个 hook。(如果是反过来的问题——hook 挡住了你想要的编辑——见下方相关阅读里”hook 误挡编辑”那篇。)

原因 6:你在 default 模式下一直点 “Just this time”

“Just this time” 按设计就不持久化。只有 “Yes, and don’t ask again” 才会写规则。如果你发现自己在反复点那个临时选项,要么点一次持久化的那个,要么切换 session 的模式。

信任项目的快速通道

在你自己、信任 Claude 判断的仓库里,别去跟一条条规则较劲——直接改模式。按 Shift+Tab 循环(defaultacceptEditsplan → 回到 default),或者持久化设定:

{
  "permissions": {
    "defaultMode": "acceptEdits",
    "deny": [
      "Bash(rm -rf *)",
      "Bash(git push --force*)",
      "Bash(curl * | bash*)"
    ]
  }
}

acceptEdits 自动接受文件编辑和工作目录内的常见文件系统命令(mkdirtouchmvcp),同时对更危险的 Bash 仍会弹窗。deny 规则照样优先,所以上面那些破坏性 pattern 仍被挡住。要在容器或 VM 里完全无人值守地跑,可以用 claude --dangerously-skip-permissions 启动(或 --permission-mode bypassPermissions),但绝不要在 agent 可能造成真实损害的机器上这么干——rm -rf /rm -rf ~ 仍会作为保险丝弹窗,但其它什么都不拦了。

如果你想在标准模式下”全放行 + 定点例外”,最干净的做法是一条裸 Bash allow 加一个 PreToolUse hook 来拒绝你要挡的少数命令,而不是去枚举每一条安全命令。

怎么确认已经修好

  • /permissions,确认你加的规则出现在列表里、且来源文件正是你预期的那个。
  • 把原本循环的每条命令各手动触发一次,确认不再弹提示。
  • 跑一条明显破坏性的命令(rm -rf /tmp/test-fixture),确认它仍会弹窗或被拒——说明 deny 规则在生效。
  • 跑一个 10 分钟探索性 session 数提示数。修完应该 <= 5 次(理想 0-2 次)。

常见坑

  • 给复合命令只写一条规则。拆开:每个 &&|; 旁的子命令各一条。
  • 用正则。Claude Code 用简单 glob(* 和 gitignore 风格的 **),不是完整正则。
  • 忘了 * 前的空格。Bash(ls*) 会连 lsof 一起匹配;Bash(ls *) 不会。
  • 给 Edit 规则用单个前导斜杠、却指望它是绝对路径。绝对路径用 //
  • 想用 Bash pattern 约束 curl 的 URL。这很脆(flag、重定向、变量全能绕过)。改成 deny 掉 curl/wget,再用 WebFetch(domain:...) allow 规则。
  • 编辑了错的范围文件、然后断言”settings 不生效”。跑 /permissions 看真正加载了什么。

FAQ

Q:“Yes, and don’t ask again”、“Yes”、“No” 有啥区别? A:“Yes, and don’t ask again” 会把规则写进 settings(持久)。单纯的 “Yes” 只放行这一次调用。误点了非持久的那个选项,是反复弹窗最常见的来源。

Q:我批准了一个带 && 的命令,它还在问。为啥? A:复合命令按子命令拆分。批准一次会给每段各存一条规则(最多 5 条),但如果各次运行里那个子命令的文本有变化,存下的规则就可能错过。给每个子命令预先写一条通配规则,比如 Bash(npm test:*)

Q:allow pattern 能用完整正则吗? A:不能。Bash 规则用 * 的 glob;Read/Edit 规则用 gitignore 的 ***。需要更复杂的逻辑就用 PreToolUse hook。

Q:怎么让 Claude Code 在某个项目里彻底不问? A:在那个项目的 .claude/settings.json 里设 "defaultMode": "acceptEdits",或者每个 session 用 Shift+Tab 切进去。容器里无人值守就用 --dangerously-skip-permissions。两种情况都把破坏性 deny 规则留着。

Q:我的 deny 规则为啥没拦住该拦的命令? A:deny 规则用的是同一套 pattern 形态。Bash(rm -rf /) 不匹配 Bash(rm -rf /tmp/x)——同族用 Bash(rm -rf *)。记住 deny 在 allow 之前求值,命中就一定赢。

Q:MCP 服务端的工具走同一套系统吗? A:走。MCP 调用以 mcp__server__tool 形式出现,可以用一样的方式 allow 或 deny,比如 mcp__puppeteer__* 覆盖 puppeteer 服务的全部工具。

相关阅读

外部参考:Claude Code 权限文档权限模式

标签: #Claude Code #agent #排查