前情提要
- 权限规则写在
settings.json里,分三类:allow直接放行,ask先问你,deny直接拒绝json{ "permissions": { "allow": ["Bash(npm run test *)"], "ask": ["Bash(git push *)"], "deny": ["Read(./.env)", "WebFetch"] } }
- 有六种权限模式:手动(default)、自动接受编辑(acceptEdits)、计划(plan)、自动(auto)、不问就拒(dontAsk)、全自主(bypassPermissions)
- 几条使用经验:allow 绕不开 deny;规则按文字匹配;全自主模式下,少数高危操作照样会问你
第3节的 mini-cc 有一个很粗的权限判定:只读的放行,其余都问。
这一节去看 Claude Code 的权限判定到底长什么样。读完你会明白一句话:规则是 Claude Code 执行的,不是模型。 也会明白它的边界在哪:写了 deny,也不一定就万无一失。
代码在 src/utils/permissions/permissions.ts。
一、一张写死的流程表

权限判定是七道关里的第 ④ 关。把那个函数(hasPermissionsToUseToolInner())简化一下,就是一张按顺序走的表:
1a. 整个工具在 deny 里 → 拒绝
1b. 整个工具在 ask 里 → 问你
1c. 工具自己检查参数 (比如 Bash 会把命令拆开,逐条去匹配你的规则)
1d. 工具说 deny → 拒绝
1f. 命中了你写的带内容的 ask 规则 → 问你(全自主模式也照问)
1g. 要改敏感路径 → 问你(全自主模式也照问)
2a. 当前是全自主模式 → 放行
2b. 整个工具在 allow 里 → 放行
3. 都没命中 → 问你
从头看到尾,你会发现一件事:没有一步是去问模型的意见。
模型在提示词里被告诉「不要 push」,那是它「想不想」的事。这张表管的是「能不能」:不管模型怎么想,走到哪一步就是哪个结果。
权限判定是一张写死的表,模型说了不算。
二、deny 为什么最先查
表的第一行就是 deny。命中了直接返回「拒绝」,后面的 allow 根本轮不到。
所以有这条经验:allow 绕不开 deny。 你在 allow 里写了 Bash(*),又在 deny 里写了 Bash(rm -rf *),后者照样生效。
这个「拒绝优先」的原则,在 Claude Code 里到处都是:
| 地方 | 怎么体现 | 在哪节 |
|---|---|---|
| 权限规则 | deny 最先查 | 本节 |
| Hook | Hook 说「放行」,也绕不开 deny 的口子 | 第9节 |
| 插件 | 插件不能通过设置给自己加权限 | 第10节 |
为什么这么设计?因为两种出错的代价不一样:该放的被拦了,你多点一次「允许」;不该放的被放了,可能是一次删库。
拒绝永远比放行优先,因为放错的代价远比拦错大。
三、六种模式,各改了哪一格
六种权限模式,在代码里其实都是同一张表。它们只是在不同的位置,把「问你」换成了别的处理:
| 模式 | 在代码里做了什么 |
|---|---|
手动 default |
什么都不改,走到第 3 步就弹窗问你 |
自动接受编辑 acceptEdits |
文件工具自己检查时(1c),对工作目录里的编辑直接放行 |
计划 plan |
不自动放行改动,再配合一条提醒让模型只读(第12节) |
自动 auto |
本来要问你的,先交给一个分类器(另一个模型)判断 |
不问就拒 dontAsk |
本来要问你的,直接拒绝 |
全自主 bypassPermissions |
在 2a 直接放行,但 1a~1g 照常生效 |
最后一行值得多看一眼。全自主模式听起来像「什么都不管」,其实它是在 2a 才放行的。排在它前面的 deny 规则、你写的 ask 规则、敏感路径检查,一个都没跳过。