Skip to content

Layer 3: 权限系统 —— 自主性与安全性的工程权衡

一个可以执行任意 shell 命令、编辑文件、调用外部 API 的 Agent,如何在「让用户尽量少操心」和「不做危险的事」之间找到平衡?

这是所有 Agent 框架设计中最关键的工程问题之一。Claude Code 的权限系统是目前业界最精细的 Agent 权限实现。


type PermissionBehavior = 'allow' | 'deny' | 'ask'

所有权限决策最终归结为三种行为:

  • allow:静默放行,用户无感知
  • deny:静默拒绝,告诉模型「权限不足」
  • ask:弹出交互式对话框,让用户决定

★ 设计洞察:注意没有 warnlog 这类「软性」行为。每个决策都是硬性的、有确定结果的。这避免了安全系统中常见的「告警疲劳」问题——如果有 warn,用户会习惯性忽略,最终等于没有安全检查。

// 外部模式(用户可选)
const EXTERNAL_PERMISSION_MODES = [
'acceptEdits', // 自动批准 CWD 内的文件编辑,其他询问
'bypassPermissions', // 跳过所有权限检查(除安全检查外)
'default', // 标准模式,需要权限时询问
'dontAsk', // 不询问,直接拒绝
'plan', // 计划模式,只允许读操作
]
// 内部模式(系统使用)
type InternalPermissionMode =
| ExternalPermissionMode
| 'auto' // ML 分类器自动审批
| 'bubble' // Fork 子 Agent,权限冒泡给父

模式层级(从最严格到最宽松):

dontAsk → plan → default → acceptEdits → auto → bypassPermissions
────────────────────────────────────────────────────────────────→
最严格 最宽松
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 类型的 Agent

utils/permissions/permissions.ts
function hasPermissionsToUseToolInner(
tool: Tool,
input: unknown,
context: ToolPermissionContext,
forceDecision?: PermissionBehavior
): PermissionResult
┌─────────────────────────────────────────────────────────┐
│ 权限决策算法 │
│ │
│ 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 模式下,以下决策不可绕过

  1. deny 规则(任何级别):显式拒绝永远生效
  2. 内容特定 ask 规则ruleBehavior: 'ask' + ruleContent):需要用户确认特定内容
  3. 安全检查classifierApprovable: false):安全分类器无法自动审批的操作

★ 设计洞察:这就是「安全不可谈判」原则。即使用户选择了最宽松的模式,某些操作仍然需要确认。例如 rm -rf / 在任何模式下都会被拦截。


// 运行时优先级从高到低
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)
对于每个规则行为(deny/ask/allow),按优先级扫描规则源:
1. 先检查所有 deny 规则(从高优先级到低)
2. 再检查所有 ask 规则
3. 最后检查 allow 规则
匹配到第一条规则就停止(短路求值)

以下工具跳过分类器检查,直接放行:

文件操作: Read, Glob, Grep, LSP, ToolSearch
任务管理: TaskCreate, TaskUpdate, TaskList, ...
交互工具: AskUserQuestion, Plan 相关
团队协调: SendMessage, TeamCreate, TeamDelete
其他: Sleep, Workflow
需要分类的工具调用
Stage 1(快速意图检测):
→ 通过模型快速判断操作是否安全
Stage 1 结果不确定?
Stage 2(深度分析):
→ 使用 thinking 模式做更深入的分析
最终结果:
{ shouldBlock: boolean, reason: string, usage: ClassifierUsage }
utils/permissions/denialTracking.ts
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()

第一次 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 规则

模式规则检查Auto-AllowAsk 行为文件编辑
dontAsk转为 denyN/A
plan提示用户只读
default提示用户需要确认
acceptEdits✓ (文件编辑)提示其他自动放行
auto✓ (分类器)分类器决策分类器决策
bypassPermissions跳到 allow跳过自动放行
bubble冒泡给父 Agent冒泡

有 warn 的系统:
warn → 用户习惯性忽略 → 等于没有安全检查 → 告警疲劳
没有 warn 的系统:
每个决策都是 allow / deny / ask
→ 用户的每次交互都是有意义的
→ 不会产生「我总是点确认」的肌肉记忆
传统系统: 默认允许,遇到危险才拦截
→ 漏网之鱼 = 安全事故
Claude Code: 默认询问,确认安全才放行
→ 漏网之鱼 = 多问了一次(用户体验代价,不是安全代价)
单独看每一层都很简单:
- deny 规则: 黑名单
- allow 规则: 白名单
- 模式: 全局开关
- 分类器: AI 判断
- Hook: 用户自定义
组合在一起的能力:
- 企业策略强制 deny 某些操作(policySettings deny 规则)
- 项目级别允许特定 npm 命令(projectSettings allow 规则)
- 用户日常使用 acceptEdits 模式
- 分类器在 auto 模式下自动审批安全操作
- Hook 在特定条件下覆盖决策
→ 每个层级独立、可组合、可覆盖

交互方向说明
← 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 事件