Book 第五章:权限为什么是治理层,不是确认框
第二部分:运行时主链路

第五章:权限为什么是治理层,不是确认框

从规则层、工具自检、mode 语义和交互编排出发,看权限系统为什么是主链路的一部分。

1. 为什么这块值得单独拆

如果只看表面,Claude Code 的权限系统像是“工具执行前弹个确认框”。

但源码往下读一层就会发现,它真正做的是一套完整的运行时治理:

  • 先把来自 settings、CLI、session、命令现场的规则统一进 ToolPermissionContext
  • 再让工具自己的 checkPermissions() 参与判断
  • 再叠加 mode 语义,比如 defaultacceptEditsbypassPermissionsplanauto
  • 再决定这次请求该走 hook、分类器、前台交互、远端桥接、频道转发,还是 headless fail-closed

所以这块对 C# 迁移特别值钱,因为它暴露了 Claude Code 一个很重要的工程判断:

权限不是 UI 功能,而是 agent runtime 的核心调度层。

相关源码锚点:

  • src/Tool.ts
  • src/types/permissions.ts
  • src/utils/permissions/permissions.ts
  • src/utils/permissions/permissionSetup.ts
  • src/utils/permissions/PermissionUpdate.ts
  • src/utils/permissions/permissionsLoader.ts
  • src/utils/permissions/permissionRuleParser.ts
  • src/hooks/useCanUseTool.tsx
  • src/hooks/toolPermission/PermissionContext.ts
  • src/hooks/toolPermission/handlers/*.ts

2. 先说总判断

我对 Claude Code 权限系统的判断是:

它不是布尔式授权,而是“规则匹配 + 工具自检 + 模式变换 + 自动审批 + 宿主交互”的组合判定器。

更具体一点,它至少同时承担五层职责:

  1. 权限配置模型
  2. 规则解析与归一化
  3. 工具调用前判定
  4. 审批交互编排
  5. 自动模式下的额外安全治理

3. 总体结构图

flowchart TD
    A["settings / CLI / session / command"] --> B["permissionSetup.ts<br/>初始化 ToolPermissionContext"]
    B --> C["alwaysAllow / alwaysDeny / alwaysAsk<br/>additionalWorkingDirectories<br/>mode flags"]

    D["tool.checkPermissions(input, ctx)"] --> E["permissions.ts<br/>hasPermissionsToUseToolInner"]
    C --> E

    E --> F{"rule / mode / tool check 结果"}
    F -- "allow" --> G["直接放行"]
    F -- "ask" --> H["useCanUseTool.tsx"]
    F -- "deny" --> I["直接拒绝"]

    H --> J["PermissionContext"]
    J --> K["hooks"]
    J --> L["bash classifier / auto classifier"]
    J --> M["interactive dialog"]
    J --> N["bridge / channel relay"]
    J --> O["swarm worker / coordinator"]

    L --> P["allow / deny / fallback"]
    K --> P
    M --> P
    N --> P
    O --> P

    P --> Q["最终 PermissionDecision"]
    Q --> R["tool.call(...) 或拒绝/中止"]

这张图里最重要的一点是:

权限结果不是某一个函数一次性算出来的,而是先做“静态裁决”,再做“动态裁决”。

4. ToolPermissionContext 才是权限运行时的中心对象

源码锚点:

  • src/Tool.ts:123
  • src/Tool.ts:140

Claude Code 把权限上下文统一收敛到了 ToolPermissionContext。它里面最重要的字段有:

  • mode
  • alwaysAllowRules
  • alwaysDenyRules
  • alwaysAskRules
  • additionalWorkingDirectories
  • isBypassPermissionsModeAvailable
  • isAutoModeAvailable
  • strippedDangerousRules
  • shouldAvoidPermissionPrompts
  • awaitAutomatedChecksBeforeDialog
  • prePlanMode

这组字段说明它的权限系统不是“当前用户有没有权限”,而是“当前这个会话处于什么执行语境”。

比如:

  • 同一个工具,在 default 模式下可能要问
  • acceptEdits 下可能直接过
  • bypassPermissions 下大部分直接过,但安全检查仍然不能绕
  • auto 下不一定问人,而是先走分类器
  • 在后台 agent 里,因为根本没有交互面,所以要直接 fail-closed

这对 C# 很重要,因为它意味着你应该把权限状态放在 ConversationRuntimeTurnContext 里,而不是做成全局静态配置。

5. 规则模型不是简单字符串,而是“源 + 行为 + 内容”

源码锚点:

  • src/types/permissions.ts
  • src/utils/permissions/permissionRuleParser.ts
  • src/utils/permissions/permissionsLoader.ts

Claude Code 的权限规则最终会落成:

type PermissionRule = {
  source: PermissionRuleSource
  ruleBehavior: 'allow' | 'deny' | 'ask'
  ruleValue: {
    toolName: string
    ruleContent?: string
  }
}

这里有三个特别值钱的设计点。

5.1 规则带来源

来源不只有 settings,还包括:

  • userSettings
  • projectSettings
  • localSettings
  • flagSettings
  • policySettings
  • cliArg
  • command
  • session

这意味着 Claude Code 没把权限规则当静态配置文件,而是当“多来源叠加状态”。

5.2 规则既能作用于整个工具,也能作用于内容

例如:

  • Bash
  • Bash(npm publish:*)
  • Agent(code-reviewer)

也就是说,它支持:

  • 工具级规则
  • 工具内容级规则
  • 某些特殊工具的子类型规则

5.3 规则字符串会被归一化

permissionRuleParser.ts 做了几件事:

  • 解析 Tool(content) 语法
  • 处理括号和反斜杠转义
  • 统一 legacy tool name 到 canonical name

这个归一化很关键,因为它让:

  • 老规则还能继续工作
  • 不同来源的规则可以可靠比较
  • 删除规则时不会因为别名不同删不掉

6. 初始化阶段干的事,比“加载配置”多得多

源码锚点:

  • src/utils/permissions/permissionSetup.ts:872
  • src/utils/permissions/permissionSetup.ts:978

initializeToolPermissionContext() 并不是简单把设置文件读出来,而是在做完整的上下文构建:

  1. 解析 CLI 里的 --allowed-tools / --disallowed-tools
  2. 处理 --tools 这类 base tool 裁剪,再自动补 deny 规则
  3. 读磁盘 settings,合并成规则集
  4. 检测 overly broad shell allow rules
  5. 如果当前是 auto mode,再额外检测 dangerous permissions
  6. 组装初始 ToolPermissionContext
  7. 加载额外工作目录
  8. 计算 bypass mode / auto mode 是否真的可用

也就是说,初始化阶段已经不是“设置装载”,而是“权限运行时引导”。

7. 真正的权限判定是分层短路的

源码锚点:

  • src/utils/permissions/permissions.ts:1158

hasPermissionsToUseToolInner() 这段代码非常值钱,因为它把 Claude Code 的权限哲学写得很清楚。

7.1 第一层:先看 deny / ask / tool-specific check

顺序大致是:

  1. 整个工具是否被 deny
  2. 整个工具是否被 ask
  3. 调工具自己的 checkPermissions()
  4. 如果工具自己返回 deny,直接拒绝
  5. 如果工具自己命中了内容级 ask 规则,直接 ask
  6. 如果命中 safety check,也直接 ask

这里最重要的点是:

工具自己也参与权限判定。

也就是 Claude Code 不是只在框架层做规则匹配,还允许工具把领域知识塞回权限系统。

典型例子:

  • BashTool 会根据子命令、sandbox、只读判断返回更细的权限结果
  • 文件类工具会把 .git/.claude/、shell 配置这类路径升级成 safetyCheck

7.2 第二层:再看 mode

如果前面没有卡住,再进入 mode 逻辑:

  • bypassPermissions 直接 allow
  • plan + isBypassPermissionsModeAvailable 也等效 bypass

但这里有个很关键的保留:

前面的 safety check 和内容级 ask,不会被 bypass mode 吃掉。

这就是 Claude Code 很典型的 fail-closed 设计。

7.3 第三层:再看 tool-wide allow

如果 mode 没直接过,再看整个工具是否被 always allow。

7.4 第四层:把 passthrough 收口成 ask

也就是说,默认态不是 allow,而是 ask。

这点特别关键,因为它决定了整个系统的安全默认值。

8. useCanUseTool 负责“静态判定之后”的动态编排

源码锚点:

  • src/hooks/useCanUseTool.tsx
  • src/hooks/toolPermission/PermissionContext.ts

hasPermissionsToUseToolInner() 给出的还只是第一轮结论,真正临门一脚是 useCanUseTool()

它做的事情可以概括成:

  • 如果已经是 allow,直接放行
  • 如果是 deny,直接记录并返回
  • 如果是 ask,进入审批编排

而审批编排本身又不是一个函数,而是一套“竞争式决策系统”。

9. PermissionContext 是审批编排的交易对象

PermissionContext 封装了整次审批过程会用到的操作:

  • 记录决策日志
  • 持久化 permission update
  • 执行 hooks
  • 触发 classifier
  • 构造 allow / deny / cancel 结果
  • 管理 confirm queue

这个对象的价值在于,它把“这次审批会发生的副作用”集中管理起来了。

所以 interactiveHandlercoordinatorHandlerswarmWorkerHandler 都不用自己拼日志、改状态、写 settings。

这对 C# 迁移的启发非常直接:

不要让 UI、分类器、hook、持久化各自改状态,应该有一个统一的 PermissionRequestContext

10. 交互审批不是单路,而是多路竞争

源码锚点:

  • src/hooks/toolPermission/handlers/interactiveHandler.ts
  • src/hooks/toolPermission/handlers/coordinatorHandler.ts
  • src/hooks/toolPermission/handlers/swarmWorkerHandler.ts

Claude Code 这块最有工程味的地方,是它不是“显示弹窗然后等用户”。

它在同时跑好几条竞争路径:

  1. 本地用户在终端里批准或拒绝
  2. hook 抢先返回 allow / deny
  3. bash classifier 抢先自动批准
  4. bridge 远端界面返回结果
  5. channel relay 通过 Telegram / iMessage 之类的渠道返回结果
  6. swarm worker 把请求转发给 leader

谁先落地,就由 createResolveOnce() 把这次请求原子地收口。

这点非常关键,因为它说明 Claude Code 的权限请求本质上是一个异步 race,而不是阻塞式对话框。

11. Auto mode 不是一个 mode 值,而是一整套附加安全层

源码锚点:

  • src/utils/permissions/permissionSetup.ts:506
  • src/utils/permissions/permissions.ts:458

Claude Code 里的 auto 最值钱的地方,不是“让模型自己批权限”,而是它围绕 classifier 做了一整圈补强。

11.1 进入 auto mode 前先剥危险规则

stripDangerousPermissionsForAutoMode() 会把这类 allow 规则先拿掉:

  • Bash(*)
  • Bash(python:*)
  • PowerShell(iex:*)
  • Agent(*)

原因很直接:

如果这些规则继续生效,就会在 classifier 之前把高风险动作提前放行,等于把 auto mode 的安全层架空。

Claude Code 还会把剥掉的规则存到 strippedDangerousRules,等离开 auto mode 时再恢复。

这一步特别有设计含金量,因为它说明作者没有把“用户配置”当绝对真理,而是允许 runtime 为了保持安全语义而临时重写配置。

11.2 classifier 前还有两个 fast path

在真正调用 auto classifier 之前,Claude Code 还会先做两轮便宜判断:

  1. 模拟 acceptEdits,如果这次操作在 acceptEdits 下本来就能过,那就不浪费 classifier 调用
  2. 如果工具本身在 auto allowlist 里,也直接过

所以它不是“所有 ask 都丢给分类器”,而是“先把确定安全的便宜路径吃掉,再把难判的交给分类器”。

11.3 classifier 失败并不是只有一种退路

permissions.ts 里能看到至少几种分支:

  • transcript 太长:退回人工审批
  • classifier unavailable:有时 fail-closed,有时 fail-open,受 gate 控制
  • 连续 denial 太多:回退到人工审批
  • headless 环境里 denial 过多:直接中止 agent

也就是说,Claude Code 对 auto mode 的理解不是“自动批准”,而是“自动治理”。

12. headless / 后台 agent 明确走 fail-closed

源码锚点:

  • src/utils/permissions/permissions.ts:922

shouldAvoidPermissionPrompts 为 true 时,Claude Code 会:

  1. 先给 PermissionRequest hooks 一个机会
  2. 如果 hook 还是没决策,就直接 deny

这块的设计思路很稳:

  • 背景 agent 没有交互面,就不要假装能补问用户
  • 但也别一上来就死拒,先给自动化 policy / hook 留口子

这非常适合未来 C# 版做 server-side / background worker 场景。

13. Claude Code 在权限系统上额外做了哪些处理

这是我觉得最值钱的一组工程细节。

13.1 规则来源分层

不是只有一个配置文件,而是多来源叠加。

13.2 规则可持久化也可会话级临时生效

PermissionUpdate 既支持写 settings,也支持只写 session。

13.3 安全检查高于 bypass

安全敏感路径不会因为 “我开了 bypass” 就自动放行。

13.4 审批过程支持输入修改

远端 bridge、用户交互、hook 都可以返回 updatedInput

这意味着审批器不是只能做 allow/deny,还能做“带修正的批准”。

13.5 审批过程支持永久化建议

工具或规则可以返回 suggestions,最后让用户顺手沉淀成规则。

13.6 自动模式会改写权限上下文

不是简单切个 mode,而是真的剥规则、记 stash、跟踪 denial。

13.7 交互宿主是可插拔的

同一套权限请求,可以去:

  • 本地 REPL
  • CCR bridge
  • channel relay
  • swarm leader

这说明权限系统和 UI 宿主已经解耦到一定程度了。

14. 对 C# 版最有价值的迁移建议

如果把这套设计翻成 C#,我建议至少拆成下面几层。

14.1 核心对象

public sealed record PermissionContextSnapshot(
    PermissionMode Mode,
    IReadOnlyDictionary<PermissionRuleSource, IReadOnlyList<PermissionRule>> AllowRules,
    IReadOnlyDictionary<PermissionRuleSource, IReadOnlyList<PermissionRule>> DenyRules,
    IReadOnlyDictionary<PermissionRuleSource, IReadOnlyList<PermissionRule>> AskRules,
    IReadOnlyDictionary<string, WorkingDirectoryGrant> AdditionalDirectories,
    bool IsBypassAvailable,
    bool IsAutoModeAvailable,
    bool ShouldAvoidPrompts,
    bool AwaitAutomatedChecksBeforeDialog,
    PermissionMode? PrePlanMode);

14.2 运行时接口

public interface IPermissionEvaluator
{
    Task<PermissionDecision> EvaluateAsync(
        ITool tool,
        IReadOnlyDictionary<string, object?> input,
        ToolUseContext context,
        CancellationToken cancellationToken);
}

public interface IPermissionRequestOrchestrator
{
    Task<PermissionDecision> ResolveAsync(
        PermissionRequest request,
        CancellationToken cancellationToken);
}

public interface IPermissionRuleStore
{
    Task<PermissionContextSnapshot> LoadAsync(CancellationToken cancellationToken);
    Task PersistAsync(IReadOnlyList<PermissionUpdate> updates, CancellationToken cancellationToken);
}

14.3 迁移时不要照抄的点

  • 不要把 React queue state 直接翻成核心逻辑
  • 不要把 PermissionContext 和 UI callback 紧耦合
  • 不要把 auto mode classifier 写死在 evaluator 里

更好的做法是:

  • evaluator 只负责静态判定
  • orchestrator 负责 race 和审批
  • classifier 作为单独策略服务注入
  • 宿主适配器负责本地 UI / Web / 远端审批界面

15. 这一块的最终价值

如果只从“功能实现”角度看,Claude Code 的权限系统已经很复杂了。

但它真正更值钱的地方在于,它回答了三个更底层的问题:

  1. agent 运行时里的权限应该建模成什么
  2. 自动审批和人工审批应该怎么共存
  3. 同一个权限请求如何跨本地、远端、后台、多 agent 场景复用

这三个问题如果在 C# 版一开始就想清楚,后面很多东西都会顺很多:

  • ToolRuntime 怎么设计
  • Host 怎么接
  • Background agent 怎么做
  • Web UI 怎么接审批流
  • Auto mode 怎么不把安全打穿

所以在我看来,这篇不是“补充说明”,而是 Claude Code 迁移蓝图里的主干文档之一。