Claude Code 的权限系统是如何工作的

作者:candyTong日期:2026/4/5

在 agent runtime 的实现里,权限从来不是外围配置项,而是执行系统的一部分。只要模型开始改文件、跑命令、调用外部工具,系统就必须回答一个核心问题:这一步是否允许执行,以及这个判断应该在执行链路的哪个位置完成。

Claude Code 的权限系统很适合作为分析样本。它不是在工具外面额外包一层确认框,而是把权限判断直接嵌入工具调用链路,让一次工具调用从发起到落地,始终伴随一套可组合、可回写的运行时裁决。

文章从一个常见工作场景切入,分析权限系统实际解决的问题、内部层次划分,以及这些设计对自建 agent 系统的参考价值。

Claude Code 的权限系统大致是在回答 4 个问题:

  • 这次工具请求本身成不成立
  • 当前上下文里,有没有针对这个工具的全局规则
  • 这个工具自己,是否还需要补充更细的风险判断
  • 如果前面都没有直接拍板,最后要怎么收口成真正的运行时结果

为了先建立这个整体直觉,可以把 Claude Code 的做法先抽象成下面这样一条执行管线。先把它当成一张“地图”就够了,具体函数名后面再一层层展开:

1%% 从上到下依次表示:输入校验、通用规则预检查、工具级检查、最终收口与执行
2flowchart TD
3  A["LLM 产出 tool_use"] --> B["输入校验<br/>validateInput()"]
4  B --> C["全局规则预检查<br/>getDenyRuleForTool()<br/>getAskRuleForTool()"]
5  C --> D["工具级检查<br/>tool.checkPermissions()"]
6  D --> E["模式与整体验证收口<br/>bypassPermissions mode<br/>toolAlwaysAllowedRule()"]
7  E --> F["形成最终裁决<br/>allow / ask / deny<br/>passthrough -> ask"]
8  F -->|"allow"| G["执行工具<br/>call()"]
9  F -->|"ask"| H["向用户确认"]
10  F -->|"deny"| I["生成拒绝结果"]
11  G --> J["回写工具结果<br/>mapToolResultToToolResultBlockParam()"]
12  I --> J
13

下面不按术语分块,而是直接围绕一个更贴近真实工作的场景往下走:

用户说:“帮我修一下 src/runtime.ts 里的一个小 bug,然后跑一次 npm test。”

这类请求很有代表性,因为它同时包含两种典型副作用:改文件,以及执行命令。

1. 第一个问题不是“让不让做”,而是“这次请求到底成不成立”

假设模型先产出了一个文件编辑请求:

1{
2  // 模型选择的工具
3  "tool": "Edit",
4  "input": {
5    // 目标文件路径
6    "file_path": "src/runtime.ts",
7    // 预期被替换的原始内容
8    "old_string": "if (x) return y",
9    // 希望写入的新内容
10    "new_string": "if (x != null) return y",
11  },
12}
13

这时一个稳妥的 runtime,通常不会立刻进入“允许还是拒绝”的判断,而是先做输入校验。对工程实现者来说,更重要的是这层职责本身:

  • 参数结构是否完整
  • 路径类型是否正确
  • 这是不是一个连工具自己都无法理解的无效请求

原因很简单。输入本身如果不成立,它就不该进入权限流程。否则你会把“坏请求”和“高风险请求”混在一起,最后既难调试,也难向模型解释失败原因。

这里最重要的心智模型是:先区分“请求是否合法”,再区分“合法请求能否执行”。

这不是为了讲解方便才硬拆出来的层次,Claude Code 的工具抽象本身就是这么设计的。下面这段代码是为了说明职责边界做的精简示意,去掉了泛型、UI 和错误处理等不影响主线的细节:

1// validateInput 的返回值只回答“输入是否合法”
2type ValidationResult =
3  | { result: true }
4  | { result: false; message: string; errorCode: number };
5
6type Tool = {
7  // 先校验输入结构和参数合法性
8  validateInput?(input, context): Promise<ValidationResult>;
9  // 再判断这次调用是否允许执行
10  checkPermissions(input, context): Promise<PermissionResult>;
11};
12

这段代码很短,但已经说明了一件事:“输入是否成立”“这次动作是否允许执行” 在 Claude Code 里从一开始就是两个不同阶段,而不是一个大而全的判断函数。

2. 输入合法之后,先做的是“全局规则预检查”

编辑请求校验通过以后,runtime 先做的不是立刻得出最终 allow / ask / deny,而是先跑一层比较通用的规则预检查。对应到 permissions.ts,比较核心的是这两类判断:

  • getDenyRuleForTool(...)
  • getAskRuleForTool(...)

它们的核心逻辑并不复杂,本质上就是两步:

  1. 先把当前上下文里的 deny rulesask rules 展开成规则列表
  2. 再用 toolMatchesRule(...) 看这些规则里,是否存在一条能匹配“整个工具”的规则

这里还需要把“规则从哪里来”说清楚。对运行时来说,getDenyRuleForTool(...)getAskRuleForTool(...) 读到的并不是硬编码常量,而是从 settings 体系装载出来的权限配置。

但对这一节来说,最重要的不是把配置系统的细枝末节一次讲完,而是先理解这一层到底在回答什么问题:

当前上下文里,有没有针对整个工具的预设规则?

一个最小例子大致是这样:

1{
2  "permissions": {
3    // 这些工具命中后可直接放行
4    "allow": ["Read", "Glob"],
5    // 这些工具命中后必须先询问
6    "ask": ["Bash", "WebFetch"],
7    // 这些工具命中后直接拒绝
8    "deny": ["Edit"],
9  },
10}
11

这些配置可以来自几类来源:

  • userSettings:全局用户配置,通常是 ~/.claude/settings.json
  • projectSettings:项目共享配置,通常是 .claude/settings.json
  • localSettings:项目本地配置,通常是 .claude/settings.local.json
  • policySettings:托管或企业级策略配置

2.1 多级配置如何合并

理解规则预检查时,不能只看“配置写在哪个文件里”,还要看这些文件最终是怎么合并成运行时上下文的。这里保留一个够用的心智模型就可以:

Claude Code 对 settings 的处理可以概括成两层:

  1. 先分别读取每个 source 的原始配置
  2. 再按优先级把它们合并成一份生效视图

对常见的文件型配置来说,核心优先级是:

userSettings -> projectSettings -> localSettings -> policySettings

这里的含义不是“后面的文件会把前面的整个 permissions 对象覆盖掉”,而是:

  • 普通标量字段,后面的值覆盖前面的值
  • 数组字段,会做拼接并去重
  • permissions.allow / ask / deny 这类权限数组,属于第二种情况

精简后的合并逻辑大致可以写成这样:

1function mergeArrays(targetArray, sourceArray) {
2  // 数组合并时不是覆盖,而是拼接后去重
3  return uniq([...targetArray, ...sourceArray]);
4}
5
6function settingsMergeCustomizer(objValue, srcValue) {
7  // 只有数组走自定义合并
8  if (Array.isArray(objValue) && Array.isArray(srcValue)) {
9    return mergeArrays(objValue, srcValue);
10  }
11
12  // 其他字段交给默认 merge 行为处理
13  return undefined;
14}
15

如果把它放到权限配置里理解,可以把结果看成这样:

1// userSettings
2{ "permissions": { "ask": ["Bash"] } }
3
4// projectSettings
5{ "permissions": { "deny": ["Edit"] } }
6
7// localSettings
8{ "permissions": { "allow": ["Read", "Glob"], "ask": ["WebFetch"] } }
9

合并后的运行时视图会更接近:

1{
2  "permissions": {
3    // 来自 localSettings
4    "allow": ["Read", "Glob"],
5    // ask 数组会叠加并去重
6    "ask": ["Bash", "WebFetch"],
7    // 来自 projectSettings
8    "deny": ["Edit"],
9  },
10}
11

policySettings 稍微特殊一点。对理解本文主线来说,你只需要知道它代表更高优先级的托管策略来源,最终也会一起进入运行时视图。

因此,权限判断里看到的 context 并不是某一个单独文件,而是一份已经经过多级来源合并后的运行时视图。

运行时会先把这些来源里的 permissions.allow / deny / ask 读出来,再转换成统一的 PermissionRule 列表。对权限判断来说,可以把这个装载过程理解成下面这样:

1function settingsJsonToRules(data, source) {
2  if (!data?.permissions) return [];
3
4  return ['allow', 'deny', 'ask'].flatMap(behavior =>
5    (data.permissions[behavior] || []).map(ruleString => ({
6      source,
7      ruleBehavior: behavior,
8      ruleValue: permissionRuleValueFromString(ruleString),
9    })),
10  );
11}
12

也就是说,配置文件里的字符串规则会先被解析成统一结构,再进入后面的匹配逻辑。设置来源本身也会被保留下来,因为后面不仅要判断“命中了哪条规则”,还经常要解释“这条规则来自哪里”。

对应的精简代码大致可以写成这样:

1function getDenyRuleForTool(context, tool) {
2  // 先取出当前上下文里的 deny 规则
3  // 再找第一条能匹配整个工具的规则
4  return (
5    getDenyRules(context).find(rule => toolMatchesRule(tool, rule)) || null
6  );
7}
8
9function getAskRuleForTool(context, tool) {
10  // ask 规则的处理方式完全对称
11  return getAskRules(context).find(rule => toolMatchesRule(tool, rule)) || null;
12}
13

这里最关键的是 toolMatchesRule(...) 这层语义:它检查的是整工具匹配,不是带内容的细粒度匹配。也就是说,这一层看的规则更像:

  • Bash
  • Edit
  • mcp__server_name

而不是这类带内容的规则:

后者属于更细粒度的内容匹配,通常要等到后面的工具级检查再处理。

比如:

  • 整个 Edit 工具是否被拒绝
  • 整个 Bash 工具是否要求先询问
  • 当前会话里有没有针对整类工具的默认限制

这一层的特点是:它偏通用,按“工具整体”或“全局规则”先做一次早筛。

所以更准确的说法不是“规则判断之后系统就已经知道最终是 allow 还是 deny 了”,而是:

  • 如果这里命中整体验证的 deny,可以直接结束
  • 如果这里命中整体验证的 ask,也可能直接要求确认
  • 如果这里没拦住,系统还要继续往下看工具自己的语义检查

这里需要特别区分的一点是:规则判断是权限流程的一层,但它不是全部。

3. 接下来才是“工具级检查”:tool.checkPermissions() 到底在做什么

假设第一步编辑已经做完,模型接着又发起一个命令:

1{
2  // 模型选择执行命令工具
3  "tool": "Bash",
4  "input": {
5    // 具体要执行的命令
6    "command": "npm test -- permissions",
7  },
8}
9

如果只靠上一节那层全局规则,通常还不够,因为命令类工具的风险不只来自“它是不是 Bash”,还来自“它实际会产生什么副作用”。

这时候就会进入 tool.checkPermissions()。它的作用不是重复跑一遍全局规则,而是让每个工具补上“只有我自己最清楚的风险语义”。

这一层同样会消费配置文件,只不过它读取的不再是“整工具规则”,而是更细粒度的内容规则。对于 Bash 来说,配置里的规则可以写成下面这种形式:

1{
2  "permissions": {
3    // 整个 Bash 工具默认先询问
4    "ask": ["Bash"],
5    // 某些具体命令模式允许直接放行
6    "allow": ["Bash(npm test:*)", "Bash(git status:*)"],
7    // 某些具体命令模式直接拒绝
8    "deny": ["Bash(rm -rf:*)", "Bash(curl * | sh:*)"],
9  },
10}
11

和第 2 节不同,这里看的不是 Bash 这个工具名本身,而是 Bash(...) 里面那段 ruleContent。运行时会先把这些内容规则按工具名分组,再交给工具自己的匹配逻辑处理。精简后的结构大致像这样:

1function getRuleByContentsForTool(context, tool, behavior) {
2  // 先筛出属于这个工具、而且带有 ruleContent 的规则
3  // 例如只取出 Bash(npm test:*) 这样的内容规则
4  return getRuleByContentsForToolName(
5    context,
6    getToolNameForPermissionCheck(tool),
7    behavior,
8  );
9}
10

Bash 来说,后面的 matchingRulesForInput(...) 会继续做一层工作:把这些内容规则分成 deny / ask / allow 三组,再根据“精确匹配”或“前缀匹配”的方式去对比当前命令。因此,第 3 节讨论的不是“配置文件是否参与”,而是配置文件里的哪一类规则会在工具级检查阶段继续生效。

更适合把它理解成“工具自己补充语义判断”:

  • 文件工具更关心路径范围、敏感文件和写入目标
  • 命令工具更关心真实执行内容,而不只是字符串表面
  • 网络工具更关心目标地址和数据外发风险

这一步里,工具可能返回四类结果:

  • deny:工具自己已经能确认这次动作不能做
  • ask:工具自己认为这次动作必须先确认
  • allow:工具自己确认可以直接放行
  • passthrough:工具自己暂时不下结论,把判断交回给通用权限流程继续收口

这里的 passthrough 很关键,因为它解释了你问的第二个问题:不是规则判断一结束,就天然只剩 allow / ask / deny 它不是“允许执行”,也不是“出错了”,而是“这个工具自己先不拍板,请上层继续判断”。在真实实现里,tool.checkPermissions() 经常先返回 passthrough,然后后面的 mode、always allow 规则或者默认提示逻辑再把它收敛成最终结果。

这也是为什么命令类工具总是最麻烦。npm test -- permissions 看起来像低风险操作,但一旦换成带重定向、子命令、复合命令的写法,字符串表面就未必等于实际效果了。更克制的说法不是“它一定按某个固定顺序做 8 步判断”,而是:一个稳妥的 runtime 会尽量先把命令整理到更容易分析的形式,再叠加工具自己的安全判断。

如果先把这一层讲成人话,它想表达的顺序其实很简单:

  • 先看有没有明确命中规则
  • 再看这条命令本身有没有更具体的风险结构
  • 如果前面都没拦住,再看有没有补充放行条件
  • 工具自己还是拿不准,就把判断交回上层继续收口

Bash 的权限检查能把这种“分层递进”的结构表现得比较完整。下面这段代码保留了 bashPermissions.ts 的判断顺序,删掉了建议规则、错误处理和边界分支:

1%% Bash 工具内部的权限判断顺序,与下面的伪代码逐行对应
2flowchart TD
3  A["进入 Bash.checkPermissions()"] --> B["精确命中规则<br/>bashToolCheckExactMatchPermission()"]
4  B -->|"deny / ask"| C["直接返回"]
5  B -->|"allow / passthrough"| D["前缀规则匹配<br/>matchingRulesForInput(..., 'prefix')"]
6  D -->|"deny / ask"| C
7  D -->|"未命中 deny / ask"| E["路径约束检查<br/>checkPathConstraints()"]
8  E -->|"deny / ask"| C
9  E -->|"passthrough"| F["判断 exact.behavior === 'allow'"]
10  F -->|"是"| C
11  F -->|"否"| G["判断 matchingAllowRules[0]"]
12  G -->|"命中 allow"| C
13  G -->|"未命中"| H["模式补充判断<br/>checkPermissionMode()"]
14  H -->|"allow / ask / deny"| C
15  H -->|"passthrough"| I["只读命令判断<br/>BashTool.isReadOnly()"]
16  I -->|"allow"| C
17  I -->|"否"| J["返回 passthrough<br/>交回通用权限流程"]
18
1export const bashToolCheckPermission = (input, permissionContext) => {
2  // 先检查最严格的“精确命中”规则
3  // 例如当前命令正好命中 Bash(npm test -- permissions)  Bash(rm -rf /tmp/demo)
4  const exact = bashToolCheckExactMatchPermission(input, permissionContext);
5  if (exact.behavior === 'deny' || exact.behavior === 'ask') return exact;
6
7  // 再检查更宽一点的前缀规则
8  // 例如 Bash(npm test:*)、Bash(git status:*)、Bash(rm -rf:*)
9  const { matchingDenyRules, matchingAskRules, matchingAllowRules } =
10    matchingRulesForInput(input, permissionContext, 'prefix');
11
12  // deny / ask 一旦命中,立即结束
13  // 例如 Bash(rm -rf:*) 命中 deny,或 Bash(npm publish:*) 命中 ask
14  if (matchingDenyRules[0]) return { behavior: 'deny' };
15  if (matchingAskRules[0]) return { behavior: 'ask' };
16
17  // 然后检查路径约束,例如是否越过工作区、是否碰到敏感路径
18  // 例如 cat ../secrets.txt、echo hi > .claude/settings.json
19  const pathResult = checkPathConstraints(input, getCwd(), permissionContext);
20  if (pathResult.behavior !== 'passthrough') return pathResult;
21
22  // 允许规则要在前面的风险检查之后再生效
23  // 例如精确规则明确允许 Bash(npm test -- permissions)
24  if (exact.behavior === 'allow') return exact;
25  // 例如前缀规则允许 Bash(npm test:*)  Bash(git status:*)
26  if (matchingAllowRules[0]) return { behavior: 'allow', updatedInput: input };
27
28  // 最后再看 mode 和“只读命令”这样的补充放行条件
29  // 例如在 bypassPermissions 模式下直接放行,或在受限模式下要求 ask
30  const modeResult = checkPermissionMode(input, permissionContext);
31  if (modeResult.behavior !== 'passthrough') return modeResult;
32
33  // 只读命令可以作为最后一道补充放行
34  // 例如 ls -la、pwd、git status
35  if (BashTool.isReadOnly(input)) {
36    return { behavior: 'allow', updatedInput: input };
37  }
38
39  // 工具自己没有最终意见时,交回通用权限流程继续收口
40  // 例如 npm test 既没命中 allow/deny/ask,也不是纯只读命令
41  return { behavior: 'passthrough' };
42};
43

这段代码最值得关注的不是语法,而是判断顺序本身:

  • 先看最明确的规则命中
  • 再看路径和命令语义这类工具特有风险
  • 最后才落到 mode 和只读判断
  • 如果工具自己也没有明确放行或拒绝,就先返回 passthrough

这也是“工具级检查”真正的意义。它不是重复造一遍全局权限系统,而是把那些只有工具自己最清楚的风险补上。

3.1 为什么 Shell 权限判断不能只看字符串

之所以这里要专门展开,是因为命令工具的“内容规则匹配”只有建立在更可靠的语法理解上,前面那套判断顺序才真正站得住。

Bash 这类工具里,光靠字符串切分通常不够,因为很多 Shell 语义只有在语法层面才能稳定区分。tree-sitter 在这里的作用,不是简单地把命令拆成 token,而是把命令解析成 AST,让系统能看到更可靠的结构信息,比如:

  • 有没有真正的操作符节点,如 &&||;
  • 有没有 pipeline、subshell、command substitution、heredoc
  • 某段内容是在引号里、在参数里,还是已经构成了独立命令结构

这类信息直接影响权限判断。下面这组例子更能说明问题:

1# 这些都包含 rm,但语义完全不同
2rm -rf /                    # 真正执行删除,极度危险
3echo "rm -rf /" > log.txt   # 写入字符串,本质上不是执行 rm
4grep "rm" history.txt       # 搜索文本,本质上不是执行 rm
5find . -name '*.tmp' -exec rm {} \;  # rm 出现在 find -exec 结构里
6

如果只做字符串匹配,这几条命令都可能因为包含 rm 而被粗暴归成一类;但从语法结构看,它们分别是:

  • 一个真正的危险删除命令
  • 一个带重定向的 echo
  • 一个文本搜索命令
  • 一个 find -exec 复合结构

再看另一组例子:

1cd src && npm test          # 顶层操作符,表示两条串联命令
2echo 'cd src && npm test'   # 只是字符串,不是串联执行
3echo $(git status --short)  #  command substitution,内部仍有可执行结构
4

这里如果只按字符去找 &&$(,也很容易误判。真正有用的不是“字符串里出现了什么符号”,而是这些符号在语法树里扮演什么角色。

tree-sitter 的价值就在于,它让系统能按语法结构理解 Shell,而不是只靠字符串切分或正则近似猜测。对于权限判断来说,这意味着系统判断的不是“这串字符像不像危险命令”,而是“这条 Shell 语句在语法上到底由哪些结构组成”。

4. 经过收口之后,系统才形成最终的 allow / ask / deny

前两节讲完以后,权限流程其实还没结束。Claude Code 在 hasPermissionsToUseToolInner(...) 里还会继续做两类收口:

  • mode 是否允许直接绕过后续权限提示,比如当前处在 bypassPermissions
  • 整个工具是否命中了 always allow 这类整体放行规则,比如 toolAlwaysAllowedRule(...)

如果前面 tool.checkPermissions() 返回的是 passthrough,后面的收口逻辑还会把它统一转换成 ask。更准确的理解应该是:

最终的 allow / ask / deny,是整条权限管线收敛后的结果,不是某一层单独拍板的结果。

这时最值得记住的不是内部函数名,而是这三个运行时结果:

  • allow:直接执行
  • ask:暂停并向用户确认
  • deny:不执行,直接生成拒绝结果

这三种结果里,最容易被误解的是 ask。它不是失败,更像“当前上下文不足以自动放行,所以把决定权抛回给用户”。如果用户确认了,请求可以重新回到同一条执行管线;如果用户拒绝,系统就把这次动作收束为一次明确的拒绝。

换句话说,权限系统不是“执行前拦一下”这么简单,它更像一个会改变后续推理状态的 runtime 分叉点。

5. 为什么“回写工具结果”这一步不能省

很多新人理解权限系统时,只盯着前半段:怎么拦,怎么放。真正让 agent 能继续工作的,往往是后半段:这次动作的结果要怎么回到模型上下文里。

继续沿着这个场景看:

  • 如果文件改动成功,模型需要知道“改好了”,这样它才会继续决定要不要跑测试、要不要总结改动。
  • 如果测试命令被拒绝,模型也需要收到一个结构化结果,知道“为什么没跑成”,以及下一步更适合改成什么动作。

更合理的 runtime 会把这类结果写回消息流。这里同样不必把实现细节写得过重,抓住抽象就够了:无论是执行成功、等待确认,还是被拒绝,系统都应该把结果结构化地送回模型。

这一段的重点也不是让你记字段名,而是看清楚:回写结果至少要把“有没有执行”“为什么没执行”“原因来自哪一层判断”这几类信息带回去。

以“命令被拒绝”为例,回写给模型的结果大致可以理解成下面这种结构:

1{
2  // 这次工具调用最终没有执行
3  "behavior": "deny",
4  // 给模型看的拒绝说明
5  "message": "Permission to use Bash with command rm -rf / has been denied.",
6  "decisionReason": {
7    // 拒绝是由规则触发的
8    "type": "rule",
9    "rule": {
10      // 规则来源,例如 localSettings / projectSettings / session
11      "source": "localSettings",
12      // 这是一条 deny 规则
13      "ruleBehavior": "deny",
14      "ruleValue": {
15        // 作用于 Bash 工具
16        "toolName": "Bash",
17        // 命中的内容规则
18        "ruleContent": "rm -rf:*",
19      },
20    },
21  },
22}
23

这个结构的关键不在字段名本身,而在它把三件事都明确带回了模型:

  • 这次动作没有执行
  • 没执行的原因是什么
  • 原因来自哪一类规则或判断

这样模型下一步才能基于失败原因继续推理,比如改用更安全的命令、向用户申请确认,或者直接解释为什么这一步被拦住了。

如果没有这一步,模型只会感知到“动作没发生”,却不知道是输入无效、权限不足,还是用户刚刚拒绝了它。后续行为就容易发散。

6. 用一段精简过的真实代码,把这条权限管线串起来

前面看的是某个具体工具内部怎么细化判断。再往上一层,Claude Code 在通用权限管线里也有一个很清楚的结构。下面这段代码同样是为了解释主流程做的精简示意,保留的是核心决策顺序,并且和开头那张流程图一一对应:

1async function hasPermissionsToUseToolInner(tool, input, context) {
2  // 1. 全局规则预检查:整个工具是否被 deny
3  const denyRule = getDenyRuleForTool(context, tool);
4  if (denyRule) {
5    return { behavior: 'deny', decisionReason: { type: 'rule' } };
6  }
7
8  // 2. 全局规则预检查:整个工具是否要求先 ask
9  const askRule = getAskRuleForTool(context, tool);
10  if (askRule) {
11    return { behavior: 'ask', decisionReason: { type: 'rule' } };
12  }
13
14  // 3. 工具级检查:交给工具自己补充语义判断
15  // 注意:这里先把输入解析成工具期望的结构
16  const parsedInput = tool.inputSchema.parse(input);
17  const toolPermissionResult = await tool.checkPermissions(
18    parsedInput,
19    context,
20  );
21
22  // deny / ask 会直接作为最终结果向上返回
23  if (toolPermissionResult.behavior === 'deny') return toolPermissionResult;
24  if (toolPermissionResult.behavior === 'ask') return toolPermissionResult;
25
26  // 4. 收口:mode 是否允许直接放行
27  if (context.mode === 'bypassPermissions') {
28    return { behavior: 'allow', updatedInput: parsedInput };
29  }
30
31  // 5. 收口:整个工具是否命中 always allow
32  if (toolAlwaysAllowedRule(context, tool)) {
33    return { behavior: 'allow', updatedInput: parsedInput };
34  }
35
36  // 6. 最终裁决:passthrough 会被收口成 ask
37  // 只有真正明确 allow 的结果,才会在这里继续保留为 allow
38  return {
39    behavior: toolPermissionResult.behavior === 'passthrough' ? 'ask' : 'allow',
40    updatedInput: parsedInput,
41  };
42}
43

如果把开头的流程图和这段代码对照着看,关系会更清楚:

  1. 输入校验
  2. 全局规则预检查
  3. 工具级检查
  4. mode / always allow 收口
  5. 形成最终 allow / ask / deny
  6. 执行工具或生成拒绝结果
  7. 回写工具结果

你会发现,真正决定行为的不是某一个单点函数,而是几层判断逐步收敛。这样设计的好处是,每一层都只负责自己最擅长的那部分语义,最后再把结果收成统一的运行时裁决。

总结

如果只看表面现象,Claude Code 的权限系统像是在工具外面加了一层“允许 / 拒绝”的开关;但顺着执行链路拆开以后,会发现它实际是一个分层运行的判断过程:先做输入校验,再做全局规则预检查,再进入工具级检查,最后把结果收口成 allow / ask / deny,并把结果回写给模型。

这套机制的关键,不在某一个单独函数,而在这些层次的分工。配置文件里针对整工具的规则、带内容的细粒度规则、Bash 自己的语义判断,以及最终裁决后的结果回写,一起构成了完整闭环。也正因为如此,权限系统讨论的从来不只是“能不能执行”,还包括“为什么被拦住”“拦住之后模型会收到什么”。

从这个角度看,Claude Code 的权限系统并不是一个独立的安全补丁,而是工具执行系统的一部分。它和配置加载、Shell 解析、规则匹配、结果回写共同组成了一条完整的执行管线;理解这一点,也就能更准确地理解前面这些判断步骤为什么要以现在这样的顺序出现。


Claude Code 的权限系统是如何工作的》 是转载文章,点击查看原文


相关推荐


阿里云服务迁移实战(二)——网关迁移与前后端分离配置
KD2026/3/27

一、背景 由于业务原因,需要把服务器从外部阿里云账号迁移到阿里云账号 原阿里云是在服务器上部署Nginx做网关,迁移后改用阿里云CLB 同时对前后端分离逻辑做梳理,调整为更高效合理的配置 二、Nginx迁移至CLB 1.采用阿里云CLB原因 高可用性:会自动做健康检查,如果服务出现问题,会自动做流量切换 自动化管理:部署后阿里云会处理CLB的监控、更新和运维,无需手动维护 2.迁移前 迁移前Nginx部署在一台ECS服务器上 3.迁移后 迁移后单独部署负载均衡CLB 4.迁移


我让 AI 操作网页之后,开始不想点按钮了
糟糕好吃2026/3/19

每天在后台系统填表单、在电商网站筛商品、在管理后台点来点去……如果有一天,你只需要说一句话,AI 就能替你干完这些活,你会不会觉得:我的双手终于可以解放了? 说实话,我第一次看到阿里开源的 PageAgent 时,脑子里蹦出的就是上面那句话。这是一个能听懂人话、然后直接帮你操作网页的小工具——不需要写脚本,不需要装插件(甚至可以用书签),只需要一行代码,或者一句话。 它让我突然意识到:我们和网页的交互方式,可能正在迎来一次真正的变革。 一、体验下三个让你“哇塞”的场景 场景一:后台系统创建用


【OpenClaw养虾】从零开始部署安装,接入QQ机器人
卷福同学2026/3/11

从零开始的养虾记 1.OpenClaw是什么 OpenClaw最近非常的火,友友们可以在各种地方刷到它,但是还是有很多人不知道这是个什么东西,能做啥 简单总结,它真正解决了一个问题:让AI从”能聊天“变成”能干活“ 2.OpenClaw能做啥 OpenClaw是一个开源的AI Agent框架,让AI拥有了手和脚,能自动执行任务、调用浏览器、操作工具等等。 以运营自媒体账号为例,用OpenClaw搭建自动化系统,AI可完成的工作: 自动选题 自动写作 自动配图 自动发文,一键发布到公众号、小


我把大脑开源给了AI
风象南2026/3/3

前段时间遇到个很烦人的问题:随着用 AI 的频率越来越高,我发现自己每天都在做重复的“填表”工作。 代码在 GitHub,笔记在语雀,灵感在手机微信备忘录。每次开一个新的 AI 对话框,我都要不厌其烦地重新给它喂背景信息:“我是谁”、“我的项目规范是什么”、信息需要从各个系统同步到AI,效率极低。 为了解决这个问题,我干脆把这些散落的东西整合了起来,建了一个纯文本的本地知识库——我叫它 AIStudio。 一开始只是想弄个集中的仓库,方便AI找到它需要的东西,但用着用着,这套架构演变成了一个我和


LeetCode 762.二进制表示中质数个计算置位:位运算(mask O(1)判断)
Tisfy2026/2/22

【LetMeFly】762.二进制表示中质数个计算置位:位运算(mask O(1)判断) 力扣题目链接:https://leetcode.cn/problems/prime-number-of-set-bits-in-binary-representation/ 给你两个整数 left 和 right ,在闭区间 [left, right] 范围内,统计并返回 计算置位位数为质数 的整数个数。 计算置位位数 就是二进制表示中 1 的个数。 例如, 21 的二进制表示 10101 有 3


Flutter 正在计划提供 Packaged AI Assets 的支持,让你的包/插件可以更好被 AI 理解和选择
恋猫de小郭2026/2/14

如何让开源项目能够持续获得资金支持,2025 - 2026 的答案肯定是紧跟 AI 。 2025 年 Dart/Flutter MCP 和 Flutter GenUI 的出现,无疑让 Flutter 在 AI 上刷新了存在感,特别是谷歌核心项目 NotebookLM 在 Flutter 上的成功,也让 Flutter 在 AI 应用场景证明了可行性,这从第三方 appfigures 提供的数据也可以有明显体现: 数据是 appfigures 分析数百万个 iOS 和 Android 应用和游


JavaScript的数据类型 —— Boolean类型
橘朵2026/2/6

Boolean(布尔值)类型有两个字面值:true和false。 这两个布尔值不同于数值,因此 true 不等于 1,false 不等于 0。 虽然布尔值只有两个,但所有其他类型的值都有相应布尔值的等价形式,可以调用特定的Boolean() 转型函数: let message = "Hello world!"; let messageAsBoolean = Boolean(message); Boolean()转型函数可以在任意类型的数据上调用,而且始终返回一个布尔值。 下面是不同类型与布尔


中文分词与文本分析实战指南
艾光远2026/1/27

1. 引言:中文分词的重要性与挑战 中文作为一门独特的语言,其词语之间没有像英文那样的空格分隔,这使得中文文本处理面临着特殊的挑战。分词是中文自然语言处理(NLP)的基础环节,直接影响后续的文本分析、情感分析、信息检索等任务的质量。 jieba作为Python中最受欢迎的中文分词工具之一,以其高效、准确和易用性赢得了广泛认可。它不仅支持多种分词模式,还提供了丰富的扩展功能,如词性标注、关键词提取等,成为了中文NLP领域的必备工具。 2. 环境准备与基础配置 2.1. 导入必要模块 在开


2026前端面试题及答案
阿芯爱编程2026/1/18

2026前端面试题及答案 HTML/CSS 部分 1. 什么是盒模型?标准盒模型和IE盒模型的区别是什么? 答案: 盒模型是CSS中用于布局的基本概念,每个元素都被表示为一个矩形盒子,由内容(content)、内边距(padding)、边框(border)和外边距(margin)组成。 区别: 标准盒模型(W3C盒子模型):width和height只包含内容(content) IE盒模型(怪异模式盒子模型):width和height包含内容(content)、内边距(padding)和边框(b


后端线上发布计划模板
uzong2026/1/10

敬畏每一行代码,敬畏每一次变更。 本模板旨在通过结构化、可验证、可回溯的方式,降低发布风险,保障系统稳定。 一、📅 发布基本信息 项目内容发布名称示例:用户中心 v2.3.0 上线发布时间2026-01-15 01:00 – 02:30发布负责人xxx协同人员xxx发布类型✅ 功能上线 / 🔁 配置变更 / 🐞 紧急修复 / ⚙️ 架构调整是否灰度发布是 / 否(若“是”,说明策略:如 5% → 20% → 100%) 二、

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客