Layer 4: Hook & MCP 扩展体系 —— Agent 框架的扩展点设计
如何在一个安全关键的 Agent 系统中设计扩展点,让用户和第三方能够自定义 Agent 行为,同时不破坏安全边界和性能保证?
这是框架设计的经典难题:开放性 vs 安全性。Claude Code 通过 Hook 事件系统和 MCP 协议集成给出了一个工程上的参考答案。
1. 扩展体系总览
Section titled “1. 扩展体系总览”┌──────────────────────────────────────────────────────────┐│ Agent 核心 (L1/L2) ││ ││ ┌──────────────────────────────────────────────────┐ ││ │ 扩展层 (L4) │ ││ │ │ ││ │ ┌────────────────┐ ┌─────────────────────┐ │ ││ │ │ Hook 系统 │ │ MCP 系统 │ │ ││ │ │ (事件驱动) │ │ (协议驱动) │ │ ││ │ │ │ │ │ │ ││ │ │ 25 种事件 │ │ 5 种传输协议 │ │ ││ │ │ 5 种 Hook 类型 │ │ 动态工具发现 │ │ ││ │ │ 安全沙箱 │ │ 资源读取 │ │ ││ │ │ 权限集成 │ │ Elicitation 交互 │ │ ││ │ └────────┬───────┘ └──────────┬──────────┘ │ ││ │ │ │ │ ││ │ └────────┬────────────────┘ │ ││ │ │ │ ││ │ 互联: Hook 可以处理 MCP Elicitation │ ││ │ 互联: Hook 可以修改 MCP 工具输出 │ ││ │ 互联: MCP 配置变更触发 ConfigChange Hook │ ││ └──────────────────────────────────────────────────┘ │└──────────────────────────────────────────────────────────┘Part A: Hook 系统
Section titled “Part A: Hook 系统”2. Hook 事件全景
Section titled “2. Hook 事件全景”2.1 事件分类(25 种)
Section titled “2.1 事件分类(25 种)”| 类别 | 事件 | 触发时机 | 能力 |
|---|---|---|---|
| 工具生命周期 | PreToolUse | 工具执行前 | 修改输入、允许/拒绝/询问权限 |
PostToolUse | 工具执行后 | 检查结果、修改 MCP 工具输出 | |
PostToolUseFailure | 工具执行失败 | 观察错误 | |
PermissionDenied | 权限被拒绝后 | 观察 | |
PermissionRequest | 权限对话框前 | 自动允许/拒绝(headless 模式) | |
| 会话生命周期 | SessionStart | 会话初始化 | 设置 watchPaths |
SessionEnd | 会话关闭 | 清理(1.5s 超时) | |
Stop / SubagentStop | Agent 响应结束前 | 观察 | |
StopFailure | API 错误终止 | 观察 | |
| 用户交互 | UserPromptSubmit | 用户提交输入 | 阻止、添加上下文 |
| 团队协作 | TeammateIdle | 子 Agent 空闲前 | 观察 |
TaskCreated / TaskCompleted | 任务状态变更 | 观察 | |
| MCP 集成 | Elicitation | MCP 请求用户输入 | 自动接受/拒绝/取消 |
ElicitationResult | 用户响应 Elicitation | 观察 | |
| 配置监控 | ConfigChange | 设置文件变更 | 观察 |
InstructionsLoaded | CLAUDE.md 加载 | 观察(含 load_reason) | |
CwdChanged | 工作目录变更 | 设置 CLAUDE_ENV_FILE | |
FileChanged | 被监控文件变更 | matcher 指定文件名 | |
| 压缩 | PreCompact | 压缩前 | 阻止、追加自定义指令 |
PostCompact | 压缩后 | 观察 | |
| 工作树 | WorktreeCreate / WorktreeRemove | Git 工作树操作 | stdout → 路径 |
| 其他 | Setup | 仓库初始化/维护 | 观察 |
Notification | 系统通知 | matcher: notification_type |
2.2 事件能力分级
Section titled “2.2 事件能力分级”观察型(只能看,不能改): PostToolUseFailure, PermissionDenied, SessionEnd, Stop, StopFailure, InstructionsLoaded, PostCompact, TaskCreated, TaskCompleted
影响型(可以添加上下文或修改行为): PreToolUse, PostToolUse, UserPromptSubmit, PermissionRequest, Elicitation, PreCompact, SessionStart, CwdChanged, FileChanged
阻止型(可以阻止操作继续): PreToolUse (exit code 2), UserPromptSubmit (exit code 2), PreCompact (exit code 2)★ 设计洞察:事件分三级能力是有意为之。观察型零风险(只是通知);影响型有限风险(可以添加上下文但不能直接修改核心流程);阻止型最高风险(exit code 2 可以阻止操作),但只给了最关键的几个事件。
3. Hook 类型(5 种)
Section titled “3. Hook 类型(5 种)”type HookCommand = | { type: 'command'; command: string; timeout?: number } // Shell 命令 | { type: 'prompt'; prompt: string; model?: string } // Claude API 调用 | { type: 'agent'; prompt: string; model?: string } // Agent 调用 | { type: 'http'; url: string; headers?: Record } // HTTP Webhook | { type: 'callback'; callback: Function } // 内部回调(仅系统使用)3.1 Command Hook(最常用)
Section titled “3.1 Command Hook(最常用)”{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "config": { "type": "command", "command": "python3 check_safety.py", "timeout": 5000 } } ] }}输入:JSON 通过 stdin 传递 输出:stdout(JSON 或文本) + exit code 环境:继承用户 shell 环境 + CLAUDE_ENV_FILE 支持
3.2 条件执行(if 字段)
Section titled “3.2 条件执行(if 字段)”{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "config": { "type": "command", "command": "check_git_safety.sh", "if": "Bash(git *)" } } ] }}if 使用权限规则语法,在进程启动之前评估。不匹配的 hook 不会被执行(节省进程启动开销)。
3.3 异步执行模式
Section titled “3.3 异步执行模式”{ "config": { "type": "command", "command": "long_running_check.sh", "async": true, "asyncRewake": true }}async: true:后台执行,不阻塞主流程asyncRewake: true:后台执行,但如果 exit code = 2,唤醒主流程处理阻止错误
4. Hook 执行模型
Section titled “4. Hook 执行模型”4.1 完整执行流程
Section titled “4.1 完整执行流程”事件触发 ↓1. 安全检查 ├─ shouldDisableAllHooksIncludingManaged() → 策略覆盖? └─ shouldSkipHookDueToTrust() → 工作区信任? ↓2. Hook 过滤 ├─ 合并来源: user + project + local + policy + plugins + session ├─ matcher 过滤(工具名、MCP 服务器名等) ├─ `if` 条件评估 └─ 返回优先级排序的 hook 列表 ↓3. 并行执行 ├─ 每个 hook 独立超时 + AbortSignal ├─ 结果按完成顺序收集 └─ 不等最慢的(非阻塞聚合) ↓4. 结果聚合 └─ AggregatedHookResult { permissionBehavior?: 'allow' | 'deny' | 'ask' updatedInput?: Record<string, unknown> additionalContexts?: string[] blockingError?: HookBlockingError updatedMCPToolOutput?: unknown ... }4.2 Exit Code 语义
Section titled “4.2 Exit Code 语义”Exit Code 0 = 成功 → stdout 显示给模型(transcript 模式)或作为附加上下文
Exit Code 2 = 阻止 → stderr 立即显示给模型(作为 blocking error) → 操作被阻止(工具不执行 / 提交被拦截)
其他 = 非阻止错误 → stderr 仅显示给用户(不给模型看) → 操作继续正常执行★ 设计洞察:为什么用 exit code 而不是 JSON 字段来表示阻止?因为 Shell 脚本是最常见的 hook 实现。
exit 2比echo '{"block":true}'简单得多,降低了 hook 作者的门槛。同时 exit code 2 不是常见的自然退出码(0=成功, 1=通用错误),不容易误触发。
4.3 快速路径优化
Section titled “4.3 快速路径优化”性能数据: 完整 Hook 执行路径: ~6.01µs / 次 快速路径(仅内部回调): ~1.8µs / 次 优化幅度: -70%
触发条件: 所有 hook 都是内部 callback(无用户 hook) → 跳过: span 创建、进度发射、AbortSignal 设置、JSON 输出解析5. Hook 与权限系统的交互
Section titled “5. Hook 与权限系统的交互”5.1 核心原则:Hook 影响但不覆盖权限
Section titled “5.1 核心原则:Hook 影响但不覆盖权限”Hook 能做的: ✓ 提供权限建议(allow / deny / ask) ✓ 修改工具输入(可能改变权限判定结果) ✓ 添加额外上下文(给模型看)
Hook 不能做的: ✗ 覆盖 deny 规则(规则永远优先) ✗ 绕过安全检查(bypass-immune 决策不受影响) ✗ 修改权限模式
决策优先级: deny 规则 > Hook deny > Hook allow > 交互式对话框 > 默认5.2 实际执行顺序
Section titled “5.2 实际执行顺序”resolveHookPermissionDecision(): 1. Hook 返回 'allow' → 跳过交互式对话框,但 deny 规则仍然检查 2. Hook 返回 'deny' → 直接拒绝 3. Hook 返回 'ask' → 强制弹出对话框(即使有 allow 规则) 4. Hook 无决策 → 走正常权限流程Part B: MCP 系统
Section titled “Part B: MCP 系统”6. MCP 传输层
Section titled “6. MCP 传输层”6.1 五种传输协议
Section titled “6.1 五种传输协议”┌────────────┬──────────────────────────────────────────┐│ 传输协议 │ 特点 │├────────────┼──────────────────────────────────────────┤│ stdio │ 本地子进程,最简单,适合本地工具 ││ sse │ HTTP Server-Sent Events,适合云服务 ││ http │ Streamable HTTP,支持重试 ││ websocket │ WebSocket + TLS/mTLS,适合持久连接 ││ sdk │ 进程内 SDK,Anthropic 内部使用 │└────────────┴──────────────────────────────────────────┘6.2 传输层的可扩展设计
Section titled “6.2 传输层的可扩展设计”// MCP SDK 定义的传输接口interface Transport { send(message: JSONRPCMessage): Promise<void> onMessage: AsyncIterable<JSONRPCMessage> close(): Promise<void>}★ 设计洞察:传输层是完全可插拔的。只需实现
Transport接口,就可以添加新的传输协议(如 gRPC、WebRTC)。这使得 MCP 系统不受限于特定的通信方式,但所有传输协议都共享同一套工具发现、权限检查和错误处理逻辑。
7. MCP 连接生命周期
Section titled “7. MCP 连接生命周期”7.1 懒初始化 + 会话缓存
Section titled “7.1 懒初始化 + 会话缓存”第一次调用 MCP 工具 ↓ensureConnectedClient(server) ↓缓存命中? → 返回缓存的 client ↓缓存未命中: 1. 解析配置(环境变量展开) 2. 选择传输协议 3. 建立连接 4. 发送 initialize 请求 5. 缓存 client + sessionId + capabilities 6. 调用 tools/list 发现工具 7. 返回 client7.2 错误恢复
Section titled “7.2 错误恢复”认证过期 (401): → McpAuthError → 状态切换为 'needs_auth' → 如果是 OAuth: 触发重新授权流程 → 如果是 Bearer: 通知用户更新 token
会话过期 (404 + JSON-RPC -32001): → 清除缓存 → 下次调用重新连接
OAuth Token 刷新: → checkAndRefreshOAuthTokenIfNeeded() → 处理 invalid_grant(token 轮换) → 处理暂时性错误(重试)7.3 认证方式
Section titled “7.3 认证方式”type McpAuthMethod = | { type: 'bearer'; token: string } // 静态 token | { type: 'oauth'; clientId: string } // OAuth 流程 | { type: 'xaa' } // 跨应用访问(内部)8. MCP 工具包装
Section titled “8. MCP 工具包装”8.1 命名规范
Section titled “8.1 命名规范”mcp__[server-name]__[tool-name]
示例: mcp__github__create_issue ← github 服务器的 create_issue 工具 mcp__slack__send_message ← slack 服务器的 send_message 工具8.2 MCPTool 包装器
Section titled “8.2 MCPTool 包装器”MCPTool = buildTool({ isMcp: true maxResultSizeChars: 100_000 ← 100KB 上限,超过持久化到磁盘
call(): 1. 查找对应的 MCP 服务器 2. 确保已连接(懒初始化) 3. 调用 tools/call JSON-RPC 4. 处理 Elicitation(如果服务器请求用户输入) 5. 截断过长结果 6. 处理二进制内容(持久化到磁盘) 7. 返回结果
checkPermissions(): → { behavior: 'passthrough' } → 交给外层权限系统处理})8.3 MCP 资源系统
Section titled “8.3 MCP 资源系统”资源是独立的工具(不是嵌套在工具执行中):
ListMcpResourcesTool: 输入: { server: string } 输出: { resources: [{ uri, name, mimeType, description }] }
ReadMcpResourceTool: 输入: { server: string, uri: string } 输出: { contents: [{ uri, text?, blobSavedTo? }] }★ 设计洞察:把 MCP 资源设计为独立的工具而非嵌套在工具执行中,带来三个好处:
- 资源可以有独立的权限规则
- 资源发现是显式的(不会在初始化时拉取所有资源)
- 资源可以使用独立的缓存策略
9. Elicitation:MCP 工具中的用户交互
Section titled “9. Elicitation:MCP 工具中的用户交互”9.1 完整流程
Section titled “9.1 完整流程”MCP 服务器调用 → 服务器需要用户输入 ↓发送 Elicitation 请求(JSON-RPC -32042) { message, requested_schema?, mode: 'form' | 'url' } ↓尝试 Hook 解决: 执行 Elicitation hooks ├─ Hook 返回 accept + content → 自动响应,无需用户交互 ├─ Hook 返回 decline/cancel → 拒绝请求 └─ Hook 无决策 → 继续到 UI ↓UI 对话框: 用户在 REPL 中看到表单/URL 用户输入响应 ↓执行 ElicitationResult hooks(观察型) ↓响应发送回 MCP 服务器 ↓工具恢复执行9.2 Headless 模式的价值
Section titled “9.2 Headless 模式的价值”场景: CI/CD 中运行 Claude Code,MCP 服务器需要 OAuth 授权
传统方式: 弹出浏览器 → 人工操作 → 超时失败
Hook 方式: Elicitation Hook 接收请求 → 从环境变量读取 token → 自动返回 { action: 'accept', content: { token: '...' } } → 零人工干预10. Hook × MCP 互联
Section titled “10. Hook × MCP 互联”10.1 PreToolUse Hook 审批 MCP 工具
Section titled “10.1 PreToolUse Hook 审批 MCP 工具”MCP 工具调用 → PreToolUse Hook(matcher: 'mcp__github__*') → Hook 检查操作安全性 → 返回 permissionDecision: 'allow' → MCP 工具直接执行(跳过用户对话框)10.2 PostToolUse Hook 修改 MCP 输出
Section titled “10.2 PostToolUse Hook 修改 MCP 输出”MCP 工具返回结果 → PostToolUse Hook → Hook 检查输出中的敏感信息 → 返回 updatedMCPToolOutput(脱敏后的结果) → 模型看到的是脱敏后的数据10.3 ConfigChange Hook 监控 MCP 配置
Section titled “10.3 ConfigChange Hook 监控 MCP 配置”.mcp.json 文件变更 → ConfigChange Hook → Hook 验证新的 MCP 服务器配置 → Hook 可以触发客户端缓存清理11. 配置优先级
Section titled “11. 配置优先级”11.1 Hook 配置来源
Section titled “11.1 Hook 配置来源”优先级从高到低:1. userSettings (~/.claude/settings.json)2. projectSettings (.claude/settings.json)3. localSettings (.claude/settings.local.json)4. policySettings (企业管理配置,只读)5. pluginHooks (插件注册)6. builtinHooks (内部系统)7. sessionHooks (内存中,临时)11.2 MCP 配置来源
Section titled “11.2 MCP 配置来源”优先级从高到低:1. .mcp.json (项目运行时配置)2. settings.json (用户/项目/本地)3. enterprise (企业管理配置)4. plugins (插件发现)MCP 去重规则:computeServerSignature() 忽略 env 和 headers,只看核心配置。
12. 设计洞察总结
Section titled “12. 设计洞察总结”扩展点的三层安全模型
Section titled “扩展点的三层安全模型”Layer 1: 信任门控 → 所有 Hook 需要工作区信任 → 企业策略可以禁用所有 Hook
Layer 2: 能力分级 → 观察型(零风险)→ 影响型(有限风险)→ 阻止型(最高风险) → 不同事件对应不同的能力等级
Layer 3: 权限不可逾越 → Hook 的权限建议不能覆盖 deny 规则 → Hook 不能提升自己的权限等级可扩展但不可篡改
Section titled “可扩展但不可篡改”能扩展的: ✓ 新的 Hook 事件类型(框架级添加) ✓ 新的 MCP 传输协议(实现 Transport 接口) ✓ 新的认证方式(添加 McpAuthMethod 变体) ✓ 新的 MCP 服务器(配置文件添加) ✓ 新的工具行为(PreToolUse Hook 修改输入)
不能篡改的: ✗ 核心 Hook 事件语义(固定契约) ✗ 权限决策层级(规则 > Hook > 默认) ✗ Exit code 语义(0/2/other 固定) ✗ MCP 协议语义(SDK 管理)Exit Code 2 的极简设计
Section titled “Exit Code 2 的极简设计”为什么不用 JSON 字段 {"block": true}?
1. Shell 脚本最常见 → exit 2 比 JSON 输出简单2. exit code 2 不是自然退出码 → 不会误触发3. stderr 自然成为错误消息 → 无需额外协议4. 与 Unix 哲学一致 → 进程退出码是通用的通信机制与其他层的交互
Section titled “与其他层的交互”| 交互方向 | 说明 |
|---|---|
| ← L1 (Agent Loop) | Stop Hooks 在循环 Phase 5 执行 |
| ← L2 (Tool 系统) | PreToolUse/PostToolUse Hooks 嵌入工具执行管线 |
| → L3 (权限系统) | Hook 提供权限建议,但规则优先级更高 |
| ← L5 (上下文管理) | PreCompact/PostCompact Hooks 允许自定义压缩行为 |
| ← L6 (子 Agent) | SubagentStop/TeammateIdle Hooks 在子 Agent 生命周期触发 |
| ↔ MCP | Hook 处理 Elicitation;Hook 修改 MCP 输出;配置变更触发 Hook |