docs: 文档优化完成

This commit is contained in:
claude-code-best
2026-04-01 17:18:48 +08:00
parent 221fb6eb05
commit c57e6ee384
7 changed files with 1077 additions and 242 deletions

View File

@@ -1,69 +1,170 @@
---
title: "权限模型 - Allow/Ask/Deny 三级权限体系"
description: "详解 Claude Code 的三级权限模型Allow 自动放行、Ask 确认对话框、Deny 直接拒绝。权限规则的层级来源优先级机制。"
keywords: ["权限模型", "Allow Ask Deny", "权限控制", "安全权限", "工具权限"]
description: "详解 Claude Code 的三级权限模型实现:基于 src/utils/permissions/permissions.ts 的规则匹配引擎、五层规则来源优先级、工具名/命令/路径三维度匹配、Denial Tracking 死循环防护、权限模式切换机制。"
keywords: ["权限模型", "Allow Ask Deny", "PermissionRule", "checkPermissions", "Denial Tracking", "权限规则"]
---
{/* 本章目标:详解权限系统的设计 */}
{/* 本章目标:基于源码揭示权限系统的完整实现 */}
## 三种权限行为
每一次工具调用,系统都会做出三种裁决之一:
| 行为 | 含义 | 典型场景 |
|------|------|---------|
| **Allow** | 自动放行,用户无感知 | Read 工具读取项目内文件 |
| **Ask** | 弹出确认对话框,等待用户决定 | Bash 执行一条未知命令 |
| **Deny** | 直接拒绝AI 收到"权限被拒"的反馈 | 尝试执行被禁止的命令 |
| 行为 | 含义 | 返回类型 | 典型场景 |
|------|------|---------|---------|
| **Allow** | 自动放行,用户无感知 | `{ behavior: 'allow', updatedInput, decisionReason }` | Read 读取项目内文件 |
| **Ask** | 弹出确认对话框 | `{ behavior: 'ask', message, suggestions, metadata }` | Bash 执行未知命令 |
| **Deny** | 直接拒绝 | `{ behavior: 'deny', message, decisionReason }` | 尝试执行被禁止的命令 |
## 权限规则的层级
这些行为由 `PermissionResult` 类型定义(`src/utils/permissions/PermissionResult.ts`)。
规则可以来自多个来源,优先级从高到低:
## 权限规则的五层来源
<Frame caption="权限规则层级(优先级从上到下递减)">
<img src="/docs/images/permission-layers.png" alt="权限层级图" />
</Frame>
规则从 5 个来源汇聚(`PERMISSION_RULE_SOURCES``permissions.ts:109`),优先级从高到低:
<Steps>
<Step title="用户会话内设置">
用户在当前对话中手动授权的("对这个工具始终允许"
</Step>
<Step title="项目配置">
项目目录中的 `.claude/settings.json`,团队共享
</Step>
<Step title="用户全局配置">
`~/.claude/settings.json`,跨项目生效
</Step>
<Step title="托管设置">
企业管理员下发的策略,用户不可覆盖
</Step>
<Step title="默认规则">
系统内置的基线规则
</Step>
</Steps>
```
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`
```typescript
{
source: PermissionRuleSource // 来自哪个层级
ruleBehavior: 'allow' | 'ask' | 'deny'
ruleValue: {
toolName: string // 如 "Bash"、"mcp__server1"
ruleContent?: string // 如 "git *"、"src/**"
}
}
```
- **工具名匹配**`"tool": "Bash"` → 针对所有 Bash 命令
- **命令模式匹配**`"command": "git *"` → 只针对 git 开头的命令
- **路径匹配**`"path": "src/**"` → 只允许操作 src 目录下的文件
- **组合条件**:以上条件可以组合使用
## 规则匹配引擎
### 三维度匹配
`permissions.ts` 实现了三种匹配维度:
**1. 工具名匹配**`toolMatchesRule()`,第 238 行)
匹配整个工具,仅当规则没有 `ruleContent`
```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:514`)解析命令模式:
```json
{"tool": "Bash", "ruleContent": "git *"} → 匹配 "git commit -m 'fix'"
```
命令通过 AST 解析(`readOnlyValidation.ts` 使用 tree-sitter bash提取第一个子命令进行匹配。
**3. 路径匹配**(文件工具的 `checkPermissions()`
Read/Edit/Write 工具通过 `getPath()` 提取文件路径,与 `ruleContent` 中的 glob 模式匹配:
```json
{"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` |
| 模式 | 适用场景 |
|------|---------|
| **Default** | 日常使用,敏感操作逐一确认 |
| **Plan Mode** | 探索阶段AI 只能读不能写 |
| **Bypass** | 完全信任 AI所有操作自动放行需显式开启 |
Plan Mode 切换由 `EnterPlanModeTool.call()` 触发:
```typescript
// EnterPlanModeTool.ts:88
context.setAppState(prev => ({
...prev,
toolPermissionContext: applyPermissionUpdate(
prepareContextForPlanMode(prev.toolPermissionContext),
{ type: 'setMode', mode: 'plan', destination: 'session' },
),
}))
```
## Denial Tracking
退出时由 `ExitPlanModeV2Tool` 恢复为之前的模式。
系统不仅记录"允许了什么",还追踪"拒绝了什么"
## Denial Tracking死循环防护
- 如果 AI 被连续拒绝多次同一类操作,系统会调整策略
- 这防止 AI 陷入"反复请求同一个被拒操作"的死循环
`src/utils/permissions/denialTracking.ts` 实现了拒绝追踪机制:
```typescript
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`
```typescript
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`。

View File

@@ -1,10 +1,10 @@
---
title: "计划模式 - Plan Mode 先看后做的安全机制"
description: "解析 Claude Code Plan Mode 设计:给 AI 一个只读探索阶段,先分析再执行,避免不可逆操作,让用户在 AI 行动前审查方案。"
keywords: ["Plan Mode", "计划模式", "只读模式", "安全执行", "预览方案"]
description: "基于源码解析 Claude Code Plan Mode 的完整实现EnterPlanModeTool/ExitPlanModeV2Tool 的工具设计、权限上下文切换机制、Prompt-based 权限请求、计划文件持久化、Teammate 审批流程。"
keywords: ["Plan Mode", "计划模式", "EnterPlanMode", "ExitPlanMode", "prepareContextForPlanMode", "allowedPrompts"]
---
{/* 本章目标:解释 Plan Mode 的设计理念 */}
{/* 本章目标:基于源码揭示 Plan Mode 的完整实现 */}
## 问题场景
@@ -12,50 +12,140 @@ keywords: ["Plan Mode", "计划模式", "只读模式", "安全执行", "预览
## Plan Mode 的解决方案
计划模式给对话加了一个"只读阶段"
计划模式给对话加了一个"只读阶段",通过两个工具实现闭环
<Steps>
<Step title="进入计划模式">
AI 自主判断任务需要规划(或用户主动触发),进入计划模式
<Step title="EnterPlanMode — 进入计划模式">
AI 自主判断(或用户触发)任务需要规划,调用 `EnterPlanModeTool``src/tools/EnterPlanModeTool/EnterPlanModeTool.ts:36`)。该工具需要**用户审批**`checkPermissions` 返回 `ask`)。
</Step>
<Step title="探索阶段">
在这个阶段AI 只能使用读取和搜索类工具——不能编辑文件、不能执行命令
<Step title="探索阶段 — 只读工具集">
权限模式切换为 `'plan'`AI 只能使用 `isReadOnly()` 为 true 的工具Read、Grep、Glob、Agent 等)。写操作被自动拒绝。
</Step>
<Step title="形成计划">
AI 把理解和方案写入计划文件提交给用户审阅
<Step title="ExitPlanMode — 提交方案审批">
AI 完成探索后,调用 `ExitPlanModeV2Tool``src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts:147`),将计划文件提交给用户审阅。这是第二个**需要用户审批**的节点。
</Step>
<Step title="用户审批">
用户阅读计划,提出修改意见或直接批准
</Step>
<Step title="退出计划模式">
计划被批准后AI 恢复全部工具权限,按计划执行
<Step title="恢复执行 — 全部工具权限">
用户批准后权限模式恢复为进入前的状态AI 按计划执行。
</Step>
</Steps>
## 权限的自动收窄与恢复
计划模式的精妙之处在于**自动改变权限上下文**
### 进入:`prepareContextForPlanMode()`
- 进入时:系统自动保存当前权限模式,切换为"只读"
- 退出时:系统自动恢复之前的权限模式
`EnterPlanModeTool.call()`(第 77 行)的核心逻辑:
AI 和用户都不需要手动调整权限设置。
```typescript
// 1. 记录转换状态(保存之前的模式)
handlePlanModeTransition(currentMode, 'plan')
// 2. 切换权限上下文为 plan 模式
context.setAppState(prev => ({
...prev,
toolPermissionContext: applyPermissionUpdate(
prepareContextForPlanMode(prev.toolPermissionContext),
{ type: 'setMode', mode: 'plan', destination: 'session' },
),
}))
```
`prepareContextForPlanMode()``src/utils/permissions/permissionSetup.ts`)做了什么:
- 创建新的 `ToolPermissionContext``mode` 设为 `'plan'`
- 在 plan 模式下,工具的 `isReadOnly()` 检查成为唯一准入条件
- 如果用户的默认模式是 `'auto'`,还会激活 classifier 的副作用
### 退出:权限恢复 + Prompt-based 权限
`ExitPlanModeV2Tool` 的退出逻辑做了两件关键的事:
**1. 恢复权限模式**
通过 `handlePlanModeTransition()` 和 `applyPermissionUpdate()` 恢复到进入前的模式。
**2. 注入 Prompt-based 权限**
这是 Plan Mode 最精妙的设计——AI 可以在计划中声明它需要执行的命令类别:
```typescript
// ExitPlanModeV2Tool 的 inputSchema
allowedPrompts: z.array(z.object({
tool: z.enum(['Bash']),
prompt: z.string().describe('Semantic description, e.g. "run tests"'),
})).optional()
```
当 AI 提交计划时,如果声明了 `allowedPrompts: [{ tool: 'Bash', prompt: 'run tests' }]`,用户批准后,"run tests" 这类 Bash 命令会被自动放行——不再需要逐个确认。
## 计划文件的持久化
计划内容被写入磁盘文件(由 `getPlanFilePath()` 确定路径),这与简单的"AI 说一段话然后开始执行"有本质区别:
1. `ExitPlanModeV2Tool` 的 `normalizeToolInput` 从磁盘读取计划内容,注入到 `input.plan` 和 `input.planFilePath`
2. 计划文件是用户**可编辑**的——用户可以在审批前修改 AI 的方案
3. `planWasEdited` 字段标记用户是否修改了计划,影响后续的 tool_result 回显
4. `persistFileSnapshotIfRemote()` 在远程场景下保存文件快照
## Teammate 场景下的计划审批
在 Agent Swarms`isAgentSwarmsEnabled()`)模式下,计划审批有额外的协作流程:
```typescript
// 如果是 Teammate 角色
if (isTeammate()) {
// 发送计划到 Team Leader 的 mailbox 等待审批
const requestId = generateRequestId()
writeToMailbox(getTeamName(), {
type: 'plan_approval_request',
plan, requestId, ...
})
// 返回 awaitingLeaderApproval: true
// Team Leader 审批后通过 mailbox 通知 Teammate
}
```
这意味着在蜂群模式下,计划可能不是由直接用户审批,而是由 Team Leader 审批。
## 什么时候该用计划模式
| 场景 | 是否需要计划 |
|------|-------------|
| 修复一个明确的 typo | 不需要,直接修 |
| 添加一个简单的函数 | 不需要 |
| 重构一个大模块 | 需要——先搞清楚影响范围 |
| 涉及多个文件的 feature | 需要——先统一方案 |
| 不确定该怎么做 | 需要——让 AI 先调研 |
`EnterPlanModeTool` 的 Prompt`src/tools/EnterPlanModeTool/prompt.ts`)定义了两套触发标准——外部版本更积极(鼓励规划),内部版本更克制(仅在真正模糊时使用):
| 场景 | 外部版本 | 内部版本 |
|------|---------|---------|
| 修复 typo | 跳过 | 跳过 |
| 添加删除按钮 | **进入**(涉及多个文件) | **跳过**(路径明确) |
| 重构认证系统 | **进入** | **进入**(高影响重构) |
| "开始做 X" | — | **跳过**(直接开始) |
| 架构决策Redis vs 内存缓存) | **进入** | **进入**(真正模糊) |
## 计划模式 + 任务系统
计划模式通常与任务系统配合使用:
1. 在计划模式中AI 把实施步骤创建为任务列表
1. 在计划模式中AI 把实施步骤创建为任务列表`TodoWrite`
2. 用户审批计划(包含任务列表)
3. 退出计划模式后AI 按任务列表逐项执行
4. 用户可以通过任务列表追踪进度
## 完整生命周期
```
用户: "重构这个模块"
AI 判断需要规划 → 调用 EnterPlanModeTool
↓ 用户审批Ask 对话框)
handlePlanModeTransition(default, 'plan') // 保存 default
prepareContextForPlanMode() // 创建只读上下文
AI 使用 Read/Grep/Glob/Agent 探索代码库
↓ (可能 10+ 轮只读工具调用)
AI 形成方案 → 调用 ExitPlanModeV2Tool({
allowedPrompts: [
{ tool: 'Bash', prompt: 'run tests' },
{ tool: 'Bash', prompt: 'install dependencies' }
]
})
↓ 用户审批计划(可编辑计划文件)
恢复权限模式 → 注入 prompt-based 权限
AI 使用全部工具执行计划,"run tests" 等命令自动放行
```