Skip to main content

三种权限行为

每一次工具调用,系统都会做出三种裁决之一:
行为含义返回类型典型场景
Allow自动放行,用户无感知{ behavior: 'allow', updatedInput, decisionReason }Read 读取项目内文件
Ask弹出确认对话框{ behavior: 'ask', message, suggestions, metadata }Bash 执行未知命令
Deny直接拒绝{ behavior: 'deny', message, decisionReason }尝试执行被禁止的命令
这些行为由 PermissionResult 类型定义(src/utils/permissions/PermissionResult.ts)。

权限规则的五层来源

规则从 5 个来源汇聚(PERMISSION_RULE_SOURCESpermissions.ts:109),优先级从高到低:
1. session        — 用户在当前对话中手动授权("Always allow")
2. cliArg         — 命令行 --allow/--deny 参数
3. command        — Skill 工具的 allowedTools 白名单
4. projectSettings — .claude/settings.json(团队共享)
5. userSettings   — ~/.claude/settings.json(跨项目)
6. policySettings — 企业管理员下发的策略(用户不可覆盖)
每个来源维护三个数组:alwaysAllowRules[source]alwaysAskRules[source]alwaysDenyRules[source] 规则数据结构为 PermissionRule
{
  source: PermissionRuleSource      // 来自哪个层级
  ruleBehavior: 'allow' | 'ask' | 'deny'
  ruleValue: {
    toolName: string                // 如 "Bash"、"mcp__server1"
    ruleContent?: string            // 如 "git *"、"src/**"
  }
}

规则匹配引擎

三维度匹配

permissions.ts 实现了三种匹配维度: 1. 工具名匹配toolMatchesRule(),第 238 行) 匹配整个工具,仅当规则没有 ruleContent
// 精确匹配
rule "Bash"匹配 BashTool
rule "mcp__server1"匹配该 MCP Server 的所有工具server 级别
rule "mcp__server1__*"通配符匹配同上
MCP 工具使用 getToolNameForPermissionCheck() 获取匹配名称,支持有前缀(mcp__server__tool)和无前缀模式。 2. 命令模式匹配(BashTool 的 checkPermissions() BashTool 通过 preparePermissionMatcher()Tool.ts:514)解析命令模式:
{"tool": "Bash", "ruleContent": "git *"}  → 匹配 "git commit -m 'fix'"
命令通过 AST 解析(readOnlyValidation.ts 使用 tree-sitter bash),提取第一个子命令进行匹配。 3. 路径匹配(文件工具的 checkPermissions() Read/Edit/Write 工具通过 getPath() 提取文件路径,与 ruleContent 中的 glob 模式匹配:
{"tool": "Edit", "ruleContent": "src/**"}  → 匹配 "src/utils/foo.ts"

权限检查的完整流程

每次工具调用的权限检查(canUseTool()checkPermissions())经过以下步骤:
1a. Blanket deny 检查
    getDenyRuleForTool() → 工具名完全匹配 deny 规则?
    ↓ 命中 → deny(工具在 getTools() 阶段就被过滤掉)

1b. Blanket allow 检查
    toolAlwaysAllowedRule() → 工具名完全匹配 allow 规则?
    ↓ 命中 → allow

2. 工具自身 checkPermissions()
    各工具有自定义逻辑:
    - BashTool: readOnlyValidation → sandbox 判定 → AST 解析 → 模式匹配
    - FileEditTool: 路径白名单检查
    - SkillTool: safe properties 白名单 + 精确/前缀匹配
    ↓ 返回 PermissionResult

3. Hook 系统
    executePermissionRequestHooks() → PreToolUse hook 可以 override
    ↓ hook 返回 deny → deny
    ↓ hook 返回 ask → 升级为 ask

4. Ask 规则检查
    getAskRules() → 命中 → ask

5. 默认行为
    根据当前 permissionMode 决定默认行为
    - 'default': 大部分工具 ask
    - 'plan': 写操作 deny,读操作 allow
    - 'bypass': 全部 allow

权限模式

模式PermissionMode适用场景行为
Default'default'日常使用敏感操作逐一确认
Plan Mode'plan'探索阶段只能读不能写(isReadOnly() 检查)
Auto'auto'信任 AI通过 transcript classifier 自动决策
Bypass'bypassPermissions'完全信任所有操作自动放行(需显式 --dangerously-skip-permissions
Plan Mode 切换由 EnterPlanModeTool.call() 触发:
// EnterPlanModeTool.ts:88
context.setAppState(prev => ({
  ...prev,
  toolPermissionContext: applyPermissionUpdate(
    prepareContextForPlanMode(prev.toolPermissionContext),
    { type: 'setMode', mode: 'plan', destination: 'session' },
  ),
}))
退出时由 ExitPlanModeV2Tool 恢复为之前的模式。

Denial Tracking:死循环防护

src/utils/permissions/denialTracking.ts 实现了拒绝追踪机制:
const DENIAL_LIMITS = {
  maxDenialsPerTool: 3,        // 同一工具连续拒绝上限
  cooldownPeriodMs: 30_000,    // 冷却期 30 秒
}
当 AI 被连续拒绝同一类操作达到上限时:
  1. recordDenial() 记录拒绝,增加计数
  2. shouldFallbackToPrompting() 检测到连续拒绝,返回 true
  3. 系统向 AI 注入消息:“Your previous tool call was rejected…”
  4. AI 被迫改变策略,避免”反复请求同一个被拒操作”的死循环
操作成功时调用 recordSuccess() 重置计数。

规则的运行时更新

权限规则可以在运行时动态更新(applyPermissionUpdate()PermissionUpdate.ts):
type PermissionUpdate =
  | { type: 'addRule', behavior, rule, destination }
  | { type: 'removeRule', behavior, rule, destination }
  | { type: 'setMode', mode, destination }
当用户在 Ask 对话框中选择 “Always allow”,系统调用 persistPermissionUpdates() 将规则写入对应层级的 settings 文件(project/user/managed),同时更新内存中的 toolPermissionContext