Layer 3: 权限系统 —— 自主性与安全性的工程权衡
一个可以执行任意 shell 命令、编辑文件、调用外部 API 的 Agent,如何在「让用户尽量少操心」和「不做危险的事」之间找到平衡?
这是所有 Agent 框架设计中最关键的工程问题之一。Claude Code 的权限系统是目前业界最精细的 Agent 权限实现。
1. 三大权限原语
Section titled “1. 三大权限原语”1.1 Permission Behavior(权限行为)
Section titled “1.1 Permission Behavior(权限行为)”type PermissionBehavior = 'allow' | 'deny' | 'ask'所有权限决策最终归结为三种行为:
- allow:静默放行,用户无感知
- deny:静默拒绝,告诉模型「权限不足」
- ask:弹出交互式对话框,让用户决定
★ 设计洞察:注意没有
warn或log这类「软性」行为。每个决策都是硬性的、有确定结果的。这避免了安全系统中常见的「告警疲劳」问题——如果有 warn,用户会习惯性忽略,最终等于没有安全检查。
1.2 Permission Mode(权限模式)
Section titled “1.2 Permission Mode(权限模式)”// 外部模式(用户可选)const EXTERNAL_PERMISSION_MODES = [ 'acceptEdits', // 自动批准 CWD 内的文件编辑,其他询问 'bypassPermissions', // 跳过所有权限检查(除安全检查外) 'default', // 标准模式,需要权限时询问 'dontAsk', // 不询问,直接拒绝 'plan', // 计划模式,只允许读操作]
// 内部模式(系统使用)type InternalPermissionMode = | ExternalPermissionMode | 'auto' // ML 分类器自动审批 | 'bubble' // Fork 子 Agent,权限冒泡给父模式层级(从最严格到最宽松):
dontAsk → plan → default → acceptEdits → auto → bypassPermissions ────────────────────────────────────────────────────────────────→ 最严格 最宽松1.3 Permission Rule(权限规则)
Section titled “1.3 Permission Rule(权限规则)”type PermissionRule = { toolName: string // 匹配工具名 ruleContent?: string // 匹配内容模式(可选) ruleBehavior: 'allow' | 'deny' | 'ask' source: RuleSource // 规则来源}规则匹配示例:
"Bash" → 匹配所有 Bash 调用"Bash(prefix:npm)" → 只匹配 npm 开头的命令"mcp__github" → 匹配 github MCP 服务器的所有工具"mcp__github__*" → 同上(通配符写法)"Agent(Explore)" → 只匹配 Explore 类型的 Agent2. 分层决策流程
Section titled “2. 分层决策流程”2.1 决策总入口
Section titled “2.1 决策总入口”function hasPermissionsToUseToolInner( tool: Tool, input: unknown, context: ToolPermissionContext, forceDecision?: PermissionBehavior): PermissionResult2.2 完整决策算法(5 步)
Section titled “2.2 完整决策算法(5 步)”┌─────────────────────────────────────────────────────────┐│ 权限决策算法 ││ ││ Step 1: Deny 规则检查 ││ ├─ 1a. 工具级 deny 规则? → DENY ││ ├─ 1b. 工具级 ask 规则? → ASK(除非 sandbox auto-allow)││ ├─ 1c. tool.checkPermissions() → 工具特定逻辑 ││ ├─ 1d. 工具返回 deny? → DENY ││ ├─ 1e. 需要交互 + ask 规则? → ASK(不可绕过) ││ ├─ 1f. 内容特定 ask 规则? → ASK(不可绕过) ││ └─ 1g. 安全检查触发? → ASK(不可绕过) ││ ││ Step 2: Mode 与 Allow 规则 ││ ├─ 2a. bypassPermissions 模式? → ALLOW ││ └─ 2b. 工具级 allow 规则? → ALLOW ││ ││ Step 3: Passthrough 转换 ││ └─ tool.checkPermissions 返回 passthrough? → ASK ││ ││ Step 4: Auto 模式(ML 分类器) ││ ├─ 安全工具白名单? → ALLOW(跳过分类器) ││ ├─ acceptEdits 快速路径(CWD 内编辑)? → ALLOW ││ └─ 运行分类器 → ALLOW 或 DENY ││ ││ Step 5: Headless Agent 模式 ││ ├─ shouldAvoidPermissionPrompts = true ││ ├─ 运行 PermissionRequest hooks → allow/deny ││ └─ 无 hook 决策 → AUTO-DENY ││ ││ 最终: behavior = 'ask' → 用户对话框 ││ behavior = 'allow' → 执行 ││ behavior = 'deny' → 拒绝 │└─────────────────────────────────────────────────────────┘2.3 不可绕过的决策(Bypass-Immune)
Section titled “2.3 不可绕过的决策(Bypass-Immune)”即使在 bypassPermissions 模式下,以下决策不可绕过:
- deny 规则(任何级别):显式拒绝永远生效
- 内容特定 ask 规则(
ruleBehavior: 'ask'+ruleContent):需要用户确认特定内容 - 安全检查(
classifierApprovable: false):安全分类器无法自动审批的操作
★ 设计洞察:这就是「安全不可谈判」原则。即使用户选择了最宽松的模式,某些操作仍然需要确认。例如
rm -rf /在任何模式下都会被拦截。
3. 规则源优先级
Section titled “3. 规则源优先级”3.1 七级规则源
Section titled “3.1 七级规则源”// 运行时优先级从高到低type RuleSource = | 'session' // 1. 临时会话规则(内存中,重启清除) | 'cliArg' // 2. CLI 参数(启动时指定) | 'command' // 3. 运行时命令(/permissions add) | 'flagSettings' // 4. Feature flags | 'policySettings' // 5. 企业策略(管理员下发,只读) | 'localSettings' // 6. 项目设置(.claude/settings.json) | 'projectSettings' // 7. 工作区设置 | 'userSettings' // 8. 用户设置(~/.claude/settings.json)3.2 规则评估顺序
Section titled “3.2 规则评估顺序”对于每个规则行为(deny/ask/allow),按优先级扫描规则源:
1. 先检查所有 deny 规则(从高优先级到低)2. 再检查所有 ask 规则3. 最后检查 allow 规则
匹配到第一条规则就停止(短路求值)4. Auto 模式:ML 分类器
Section titled “4. Auto 模式:ML 分类器”4.1 安全工具白名单
Section titled “4.1 安全工具白名单”以下工具跳过分类器检查,直接放行:
文件操作: Read, Glob, Grep, LSP, ToolSearch任务管理: TaskCreate, TaskUpdate, TaskList, ...交互工具: AskUserQuestion, Plan 相关团队协调: SendMessage, TeamCreate, TeamDelete其他: Sleep, Workflow4.2 分类器管线
Section titled “4.2 分类器管线”需要分类的工具调用 ↓Stage 1(快速意图检测): → 通过模型快速判断操作是否安全 ↓Stage 1 结果不确定? ↓Stage 2(深度分析): → 使用 thinking 模式做更深入的分析 ↓最终结果: { shouldBlock: boolean, reason: string, usage: ClassifierUsage }4.3 拒绝追踪与熔断
Section titled “4.3 拒绝追踪与熔断”type DenialTrackingState = { consecutiveDenials: number // 连续拒绝次数(allow 后重置) totalDenials: number // 累计拒绝次数 lastDenialTime?: number // 最近拒绝时间}
// 熔断逻辑:if (consecutiveDenials >= threshold) { // 分类器可能出了问题 // 回退到交互式提示(让用户决定)}★ 设计洞察:拒绝追踪的熔断机制防止了分类器的「死循环」——如果分类器连续拒绝多次,可能是分类器本身有问题(而不是操作真的危险),此时回退到人工决策更安全。
5. 权限在工具执行管线中的位置
Section titled “5. 权限在工具执行管线中的位置”API 返回 tool_use block ↓工具查找 → 输入校验 ↓runPreToolUseHooks() → Hook 可能返回权限决策 ↓resolveHookPermissionDecision() → 使用 Hook 决策(如果有) → 否则调用 canUseTool() ↓ hasPermissionsToUseTool() ← 本文档描述的核心函数 → 五步决策算法 → 返回 allow / deny / ask ↓ask → 显示权限对话框 → 用户选择: ├─ "Allow once" → session 规则,本次放行 ├─ "Allow always" → userSettings 规则,永久放行 ├─ "Deny" → 拒绝,模型看到错误 └─ "Don't ask" → 拒绝 + session deny 规则 ↓allow → tool.call() ↓runPostToolUseHooks()6. 权限对话框的渐进式信任
Section titled “6. 权限对话框的渐进式信任”第一次 git push: ┌────────────────────────────────────┐ │ Bash: git push origin main │ │ │ │ [Allow once] [Allow always] [Deny] │ └────────────────────────────────────┘
用户选择 "Allow always" → 写入 userSettings: { toolName: "Bash", ruleContent: "prefix:git push", ruleBehavior: "allow" }
之后所有 git push → 自动放行(命中 allow 规则)升级路径:
ask (每次询问) → "Allow once" → session 规则(本次会话有效) → "Allow always" → userSettings 规则(永久有效)
降级路径: → "Deny" → 本次拒绝 → "Don't ask for this" → session deny 规则7. 权限模式行为矩阵
Section titled “7. 权限模式行为矩阵”| 模式 | 规则检查 | Auto-Allow | Ask 行为 | 文件编辑 |
|---|---|---|---|---|
dontAsk | ✓ | ✗ | 转为 deny | N/A |
plan | ✓ | ✗ | 提示用户 | 只读 |
default | ✓ | ✗ | 提示用户 | 需要确认 |
acceptEdits | ✓ | ✓ (文件编辑) | 提示其他 | 自动放行 |
auto | ✓ | ✓ (分类器) | 分类器决策 | 分类器决策 |
bypassPermissions | 跳到 allow | ✓ | 跳过 | 自动放行 |
bubble | ✓ | ✗ | 冒泡给父 Agent | 冒泡 |
8. 设计洞察总结
Section titled “8. 设计洞察总结”为什么没有 warn 行为?
Section titled “为什么没有 warn 行为?”有 warn 的系统: warn → 用户习惯性忽略 → 等于没有安全检查 → 告警疲劳
没有 warn 的系统: 每个决策都是 allow / deny / ask → 用户的每次交互都是有意义的 → 不会产生「我总是点确认」的肌肉记忆Deny-by-default 的工程哲学
Section titled “Deny-by-default 的工程哲学”传统系统: 默认允许,遇到危险才拦截 → 漏网之鱼 = 安全事故
Claude Code: 默认询问,确认安全才放行 → 漏网之鱼 = 多问了一次(用户体验代价,不是安全代价)分层决策的组合爆发力
Section titled “分层决策的组合爆发力”单独看每一层都很简单: - deny 规则: 黑名单 - allow 规则: 白名单 - 模式: 全局开关 - 分类器: AI 判断 - Hook: 用户自定义
组合在一起的能力: - 企业策略强制 deny 某些操作(policySettings deny 规则) - 项目级别允许特定 npm 命令(projectSettings allow 规则) - 用户日常使用 acceptEdits 模式 - 分类器在 auto 模式下自动审批安全操作 - Hook 在特定条件下覆盖决策
→ 每个层级独立、可组合、可覆盖与其他层的交互
Section titled “与其他层的交互”| 交互方向 | 说明 |
|---|---|
| ← L2 (Tool 系统) | 工具执行管线中 canUseTool() 触发权限检查 |
| ← L1 (Agent Loop) | QueryParams.canUseTool 注入权限检查函数 |
| ← L4 (Hook 系统) | PreToolUse Hook 可提供权限决策;PermissionRequest Hook 在 headless 模式下代理决策 |
| → L6 (子 Agent) | 权限冒泡(bubble)/ 桥接(UI bridge)/ 独立(spawn) |
| ← 配置系统 | 规则从 settings.json、CLI args、策略服务器加载 |
| → 遥测系统 | 每个权限决策发射 OTel tool_decision 事件 |