2026-06-08 23:08:57 +08:00
|
|
|
|
---
|
2026-06-09 23:15:17 +08:00
|
|
|
|
tags: [权限模型, Allow-Ask-Deny, PermissionRule, Claude-Code, 安全]
|
|
|
|
|
|
create time: 2026-06-09 22:30
|
2026-06-08 23:08:57 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
# 权限模型 - Allow/Ask/Deny 三级权限体系
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
## 概述
|
|
|
|
|
|
|
|
|
|
|
|
Claude Code 的权限模型基于三级裁决(Allow/Ask/Deny),通过五层规则来源优先级、工具名/命令/路径三维度匹配引擎,以及 Denial Tracking 死循环防护机制,实现精细化的工具调用权限控制。
|
|
|
|
|
|
|
|
|
|
|
|
## 正文
|
|
|
|
|
|
|
|
|
|
|
|
### 三种权限行为
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
|
|
|
|
|
每一次工具调用,系统都会做出三种裁决之一:
|
|
|
|
|
|
|
|
|
|
|
|
| 行为 | 含义 | 返回类型 | 典型场景 |
|
|
|
|
|
|
|------|------|---------|---------|
|
|
|
|
|
|
| **Allow** | 自动放行,用户无感知 | `{ behavior: 'allow', updatedInput, decisionReason }` | Read 读取项目内文件 |
|
|
|
|
|
|
| **Ask** | 弹出确认对话框 | `{ behavior: 'ask', message, suggestions, metadata }` | Bash 执行未知命令 |
|
|
|
|
|
|
| **Deny** | 直接拒绝 | `{ behavior: 'deny', message, decisionReason }` | 尝试执行被禁止的命令 |
|
|
|
|
|
|
|
|
|
|
|
|
这些行为由 `PermissionResult` 类型定义(`src/utils/permissions/PermissionResult.ts`)。
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
### 权限规则的来源
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
|
|
|
|
|
规则从 8 个来源汇聚(`PERMISSION_RULE_SOURCES`,`permissions.ts:109`),优先级从低到高(后者覆盖前者):
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
```text
|
2026-06-08 23:08:57 +08:00
|
|
|
|
1. userSettings — ~/.claude/settings.json(跨项目)
|
|
|
|
|
|
2. projectSettings — .claude/settings.json(团队共享)
|
|
|
|
|
|
3. localSettings — .claude/settings.local.json(gitignored,个人覆盖)
|
|
|
|
|
|
4. flagSettings — --settings 命令行参数
|
|
|
|
|
|
5. policySettings — 企业管理员下发的策略(用户不可覆盖)
|
|
|
|
|
|
6. cliArg — 命令行 --allow/--deny 参数
|
|
|
|
|
|
7. command — Skill 工具的 allowedTools 白名单
|
|
|
|
|
|
8. session — 用户在当前对话中手动授权("Always allow")
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
每个来源维护三个数组:`alwaysAllowRules[source]`、`alwaysAskRules[source]`、`alwaysDenyRules[source]`。
|
|
|
|
|
|
|
|
|
|
|
|
规则数据结构为 `PermissionRule`:
|
2026-06-09 23:15:17 +08:00
|
|
|
|
|
2026-06-08 23:08:57 +08:00
|
|
|
|
```typescript
|
|
|
|
|
|
{
|
|
|
|
|
|
source: PermissionRuleSource // 来自哪个层级
|
|
|
|
|
|
ruleBehavior: 'allow' | 'ask' | 'deny'
|
|
|
|
|
|
ruleValue: {
|
|
|
|
|
|
toolName: string // 如 "Bash"、"mcp__server1"
|
|
|
|
|
|
ruleContent?: string // 如 "git *"、"src/**"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
### 规则匹配引擎
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
#### 三维度匹配
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
|
|
|
|
|
`permissions.ts` 实现了三种匹配维度:
|
|
|
|
|
|
|
|
|
|
|
|
**1. 工具名匹配**(`toolMatchesRule()`,第 238 行)
|
|
|
|
|
|
|
|
|
|
|
|
匹配整个工具,仅当规则没有 `ruleContent`:
|
2026-06-09 23:15:17 +08:00
|
|
|
|
|
2026-06-08 23:08:57 +08:00
|
|
|
|
```typescript
|
|
|
|
|
|
// 精确匹配
|
|
|
|
|
|
rule "Bash" → 匹配 BashTool
|
|
|
|
|
|
rule "mcp__server1" → 匹配该 MCP Server 的所有工具(server 级别)
|
|
|
|
|
|
rule "mcp__server1__*" → 通配符匹配(同上)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
MCP 工具使用 `getToolNameForPermissionCheck()` 获取匹配名称,支持有前缀(`mcp__server__tool`)和无前缀模式。
|
|
|
|
|
|
|
|
|
|
|
|
**2. 命令模式匹配**(BashTool 的 `checkPermissions()`)
|
|
|
|
|
|
|
|
|
|
|
|
BashTool 通过 `preparePermissionMatcher()`(`Tool.ts:520`)解析命令模式:
|
2026-06-09 23:15:17 +08:00
|
|
|
|
|
2026-06-08 23:08:57 +08:00
|
|
|
|
```json
|
|
|
|
|
|
{"tool": "Bash", "ruleContent": "git *"} → 匹配 "git commit -m 'fix'"
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
命令通过 AST 解析(`readOnlyValidation.ts` 使用 tree-sitter bash),提取第一个子命令进行匹配。
|
|
|
|
|
|
|
|
|
|
|
|
**3. 路径匹配**(文件工具的 `checkPermissions()`)
|
|
|
|
|
|
|
|
|
|
|
|
Read/Edit/Write 工具通过 `getPath()` 提取文件路径,与 `ruleContent` 中的 glob 模式匹配:
|
2026-06-09 23:15:17 +08:00
|
|
|
|
|
2026-06-08 23:08:57 +08:00
|
|
|
|
```json
|
|
|
|
|
|
{"tool": "Edit", "ruleContent": "src/**"} → 匹配 "src/utils/foo.ts"
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
#### 权限检查的完整流程
|
|
|
|
|
|
|
|
|
|
|
|
每次工具调用的权限检查(`canUseTool()` -> `checkPermissions()`)经过以下步骤:
|
|
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
|
flowchart TD
|
|
|
|
|
|
A["1a. Blanket deny 检查"] -->|"命中"| DENY["deny"]
|
|
|
|
|
|
A -->|"未命中"| B["1b. Blanket allow 检查"]
|
|
|
|
|
|
B -->|"命中"| ALLOW["allow"]
|
|
|
|
|
|
B -->|"未命中"| C["2. 工具自身 checkPermissions()"]
|
|
|
|
|
|
C --> D["3. Hook 系统\nexecutePermissionRequestHooks()"]
|
|
|
|
|
|
D -->|"hook 返回 deny"| DENY
|
|
|
|
|
|
D -->|"hook 返回 ask"| ASK["ask"]
|
|
|
|
|
|
D -->|"通过"| E["4. Ask 规则检查"]
|
|
|
|
|
|
E -->|"命中"| ASK
|
|
|
|
|
|
E -->|"未命中"| F["5. 默认行为\n根据 permissionMode 决定"]
|
2026-06-08 23:08:57 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
各工具有自定义逻辑:
|
|
|
|
|
|
- **BashTool**: readOnlyValidation -> sandbox 判定 -> AST 解析 -> 模式匹配
|
|
|
|
|
|
- **FileEditTool**: 路径白名单检查
|
|
|
|
|
|
- **SkillTool**: safe properties 白名单 + 精确/前缀匹配
|
|
|
|
|
|
|
|
|
|
|
|
### 权限模式
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
|
|
|
|
|
| 模式 | `PermissionMode` 值 | 适用场景 | 行为 |
|
|
|
|
|
|
|------|---------------------|---------|------|
|
|
|
|
|
|
| **Default** | `'default'` | 日常使用 | 敏感操作逐一确认 |
|
|
|
|
|
|
| **Plan Mode** | `'plan'` | 探索阶段 | 只能读不能写(`isReadOnly()` 检查) |
|
|
|
|
|
|
| **Accept Edits** | `'acceptEdits'` | 快速迭代 | 工作区内文件编辑自动放行,其他操作仍需确认 |
|
|
|
|
|
|
| **Don't Ask** | `'dontAsk'` | 减少打断 | 尽量自动决策,减少确认弹窗 |
|
|
|
|
|
|
| **Auto** | `'auto'` | 信任 AI | 通过 transcript classifier 自动决策(需 `TRANSCRIPT_CLASSIFIER` feature flag) |
|
2026-06-09 23:15:17 +08:00
|
|
|
|
| **Bypass** | `'bypassPermissions'` | 完全信任 | 所作操作自动放行(需显式 `--dangerously-skip-permissions`) |
|
|
|
|
|
|
|
|
|
|
|
|
> [!info] Plan Mode 切换
|
|
|
|
|
|
> Plan Mode 切换由 `EnterPlanModeTool.call()` 触发,退出时由 `ExitPlanModeV2Tool` 恢复为之前的模式。详见 [[plan-mode|计划模式]]。
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// EnterPlanModeTool.ts:88
|
|
|
|
|
|
context.setAppState(prev => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
toolPermissionContext: applyPermissionUpdate(
|
|
|
|
|
|
prepareContextForPlanMode(prev.toolPermissionContext),
|
|
|
|
|
|
{ type: 'setMode', mode: 'plan', destination: 'session' },
|
|
|
|
|
|
),
|
|
|
|
|
|
}))
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
### Denial Tracking:死循环防护
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
|
|
|
|
|
`src/utils/permissions/denialTracking.ts` 实现了拒绝追踪机制:
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
const DENIAL_LIMITS = {
|
|
|
|
|
|
maxConsecutive: 3, // 同一工具连续拒绝上限
|
|
|
|
|
|
maxTotal: 20, // 总拒绝上限
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
当 AI 被连续拒绝同一类操作达到上限时:
|
2026-06-09 23:15:17 +08:00
|
|
|
|
|
2026-06-08 23:08:57 +08:00
|
|
|
|
1. `recordDenial()` 记录拒绝,增加计数
|
|
|
|
|
|
2. `shouldFallbackToPrompting()` 检测到连续拒绝,返回 true
|
|
|
|
|
|
3. 系统向 AI 注入消息:"Your previous tool call was rejected..."
|
|
|
|
|
|
4. AI 被迫改变策略,避免"反复请求同一个被拒操作"的死循环
|
|
|
|
|
|
|
|
|
|
|
|
操作成功时调用 `recordSuccess()` 重置计数。
|
|
|
|
|
|
|
2026-06-09 23:15:17 +08:00
|
|
|
|
### 规则的运行时更新
|
2026-06-08 23:08:57 +08:00
|
|
|
|
|
|
|
|
|
|
权限规则可以在运行时动态更新(`applyPermissionUpdate()`,`PermissionUpdate.ts`):
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
type PermissionUpdate =
|
|
|
|
|
|
| { type: 'addRules', destination, rules, behavior }
|
|
|
|
|
|
| { type: 'replaceRules', destination, rules, behavior }
|
|
|
|
|
|
| { type: 'removeRules', destination, rules, behavior }
|
|
|
|
|
|
| { type: 'setMode', destination, mode }
|
|
|
|
|
|
| { type: 'addDirectories', destination, directories }
|
|
|
|
|
|
| { type: 'removeDirectories', destination, directories }
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
当用户在 Ask 对话框中选择 "Always allow",系统调用 `persistPermissionUpdates()` 将规则写入对应层级的 settings 文件(project/user/managed),同时更新内存中的 `toolPermissionContext`。
|
2026-06-09 23:15:17 +08:00
|
|
|
|
|
|
|
|
|
|
## 关联笔记
|
|
|
|
|
|
|
|
|
|
|
|
- [[why-safety-matters|AI 安全至关重要]]
|
|
|
|
|
|
- [[auto-mode|Auto Mode]]
|
|
|
|
|
|
- [[plan-mode|计划模式]]
|
|
|
|
|
|
- [[sandbox|沙箱机制]]
|
|
|
|
|
|
- [[hooks|Hooks 生命周期钩子]]
|