职责概述

解决的问题:CC 的核心流程是"模型回复→执行工具→返回结果",但用户经常需要在特定时机插入自定义逻辑——比如工具执行前做安全检查、提交代码后自动格式化、MCP 断连时发通知。Hook 系统就是这些"自定义插入点"的统一框架。

应用场景:① 每次工具执行前运行自定义安全检查脚本 ② 代码提交后自动运行 lint 和格式化 ③ MCP 连接状态变化时弹出桌面通知 ④ 权限请求时的自定义审批逻辑 ⑤ UI 组件通过 React Hooks 订阅状态变化和用户事件。

一句话理解:就像 Git Hooks——你在 commit 前自动跑测试,push 前自动检查。CC 的 Hooks 是整个生命周期的可编程拦截点。

架构设计

通知 Hooks(hooks/notifs/)
useMcpConnectivityStatus
usePluginAutoupdateNotification
useRateLimitWarningNotification
useStartupNotification
useSettingsErrors
核心协调 Hooks
useManagePlugins.ts
useMergedTools.ts
useNotifyAfterTimeout.ts
useGlobalKeybindings.tsx
useTextInput.ts
权限决策 Hooks(hooks/toolPermission/)
interactiveHandler.ts
coordinatorHandler.ts
swarmWorkerHandler.ts
PermissionContext.ts
服务订阅 Hooks
useDirectConnect.ts
useSSHSession.ts
useIDEIntegration.tsx
useRemoteSession.ts
useQueueProcessor.ts
UI 基础 Hooks
useTerminalSize.ts
useElapsedTime.ts
useAfterFirstRender.ts
useTimeout.ts
useDoublePress.ts

核心数据流

Hook 生命周期(useNotifyAfterTimeout)

1. Hook 调用
组件渲染时调用 useNotifyAfterTimeout(message, notificationType)
2. 重置交互时间
useEffect 立即调用 updateLastInteractionTime(true),避免长时间请求完成后立即弹出通知
3. 轮询检查
setInterval 每 6 秒检查 shouldNotify():用户是否在最近 6 秒内无交互
4. 发送通知
条件满足时调用 sendNotification(),通过终端通知和/或桌面通知提示用户

插件管理 Hook 数据流

1. Mount 加载
useManagePlugins 在组件挂载时调用 initialPluginLoad()
2. 加载所有插件
loadAllPlugins() 返回 { enabled, disabled, errors },同时执行 delist 检查
3. 组件并发加载
并发加载 commands、agents、hooks、MCP servers、LSP servers,错误收集到 errors[]
4. 状态写入
setAppState() 更新 plugins.enabled/disabled/commands/errors,触发 MCP 重连
5. 变更检测
needsRefresh 变为 true 时显示通知,等待用户执行 /reload-plugins

工具合并决策树

assembleToolPool()
获取内置工具列表 getTools(),合并传入的 mcpTools
应用 deny 规则
根据 toolPermissionContext 中的 deny 规则过滤工具池
mergeAndFilterTools()
将 initialTools(带优先级)与 assembled pool 合并,去重同名工具
返回最终 Tools
useMemo 缓存结果,仅在依赖变更时重新计算

关键类型与接口

useNotifyAfterTimeout Hook

// hooks/useNotifyAfterTimeout.ts:38-65
export function useNotifyAfterTimeout(
  message: string,
  notificationType: string,
): void {
  const terminal = useTerminalNotification()

  // 重置交互时间(防止长请求完成后立即通知)
  useEffect(() => {
    updateLastInteractionTime(true)
  }, [])

  useEffect(() => {
    let hasNotified = false
    const timer = setInterval(() => {
      if (shouldNotify(DEFAULT_INTERACTION_THRESHOLD_MS) && !hasNotified) {
        hasNotified = true
        clearInterval(timer)
        void sendNotification({ message, notificationType }, terminal)
      }
    }, DEFAULT_INTERACTION_THRESHOLD_MS) // 6秒轮询
    return () => clearInterval(timer)
  }, [message, notificationType, terminal])
}

useManagePlugins Hook 签名

// hooks/useManagePlugins.ts:37-39
export function useManagePlugins({
  enabled = true,
}: {
  enabled?: boolean
} = {})

// 返回 void — 通过 setAppState 副作用更新全局状态
// 主要效果:
// - 加载所有插件到 AppState.plugins
// - 运行 delist 检查和 flag 通知
// - 遥测 tengu_plugins_loaded 事件

useMergedTools Hook

// hooks/useMergedTools.ts:20-44
export function useMergedTools(
  initialTools: Tools,
  mcpTools: Tools,
  toolPermissionContext: ToolPermissionContext,
): Tools {
  return useMemo(() => {
    // 组装工具池:内置 + MCP,应用 deny 规则和去重
    const assembled = assembleToolPool(toolPermissionContext, mcpTools)
    return mergeAndFilterTools(
      initialTools, assembled, toolPermissionContext.mode,
    )
  }, [initialTools, mcpTools, toolPermissionContext,
      replBridgeEnabled, replBridgeOutboundOnly])
}

通知 Hooks 列表

// hooks/notifs/ — 集中的通知 Hook 模块
useAutoModeUnavailableNotification  // Auto Mode 不可用提示
useCanSwitchToExistingSubscription  // 订阅切换建议
useDeprecationWarningNotification   // 弃用警告
useFastModeNotification             // 快速模式提示
useIDEStatusIndicator               // IDE 连接状态指示
useInstallMessages                  // 安装消息显示
useLspInitializationNotification    // LSP 初始化状态
useMcpConnectivityStatus            // MCP 连接状态通知
useModelMigrationNotifications      // 模型迁移通知
useNpmDeprecationNotification       // npm 弃用通知
usePluginAutoupdateNotification     // 插件自动更新通知
usePluginInstallationStatus         // 插件安装进度
useRateLimitWarningNotification     // 速率限制警告
useSettingsErrors                   // 设置错误提示
useStartupNotification              // 启动消息
useTeammateShutdownNotification     // 队友关闭通知

设计模式与亮点

集中式通知 Hook 模式

hooks/notifs/ 目录将所有通知逻辑集中管理,每个通知都是一个独立的 React Hook。这种模式的好处:(1) 通知逻辑与 UI 组件解耦;(2) 可以在任意组件树的任意层级启用/禁用通知;(3) 通知的状态(是否已显示、去重)封装在 Hook 内部。典型用法是顶层 App 组件同时挂载所有通知 Hook。

idle-detection 通知策略

useNotifyAfterTimeout 实现了智能的通知时机选择:只在用户空闲超过 6 秒时才发送桌面通知。这避免了用户正在打字时弹出干扰通知。交互时间追踪由 App.tsx 的 processKeysInBatch 函数更新,避免与主 stdin 监听器冲突。

useMemo 工具池缓存

useMergedTools 通过 useMemo 缓存工具池计算结果。依赖项包括 initialTools、mcpTools、toolPermissionContext。由于 MCP 工具列表在连接/断开时会变化(通过 AppState.mcp.tools),这个缓存确保只在工具列表实际变更时重新计算,避免每次渲染都重新组装。

错误隔离与收集

useManagePlugins(L70-109)对每个组件加载(commands、agents、hooks、MCP、LSP)都用独立的 try-catch 包裹。单个组件的加载失败不会阻止其他组件加载,错误被推入 errors[] 数组并在 Doctor UI 中显示。同时与已有的 LSP 错误合并,去重后写入 AppState。

50+ Hooks 的分类体系:Claude Code 的 hooks/ 目录包含 50 多个自定义 React Hook,可按职责分为以下类别:(1) 通知类(notifs/)—— 16 个通知 Hook;(2) 状态管理类 —— useManagePlugins、useMergedTools、useAppState 等;(3) 输入类 —— useTextInput、useVimInput、useArrowKeyHistory;(4) 集成类 —— useIDEIntegration、useDirectConnect、useSSHSession;(5) UI 基础类 —— useTerminalSize、useElapsedTime、useAfterFirstRender。

开发者实践指南

添加新的通知 Hook

按照 hooks/notifs/ 目录中现有 Hook 的模式创建:

  1. hooks/notifs/ 下创建新文件(如 useMyNotification.tsx
  2. 使用 useNotifications() 获取 addNotification 函数
  3. useEffectuseAppState 订阅触发条件
  4. 在顶层 App 组件中挂载新 Hook
// hooks/notifs/useMyNotification.tsx
import { useEffect } from 'react'
import { useNotifications } from '../../context/notifications.js'
import { useAppState } from '../../state/AppState.js'

export function useMyNotification() {
  const someCondition = useAppState(s => s.someField)
  const { addNotification } = useNotifications()

  useEffect(() => {
    if (someCondition) {
      addNotification({
        key: 'my-notification',
        text: 'Something happened!',
        color: 'warning',
        priority: 'high',
      })
    }
  }, [someCondition, addNotification])
}

理解工具合并链

工具池的组装链:getTools()(内置工具)+ mcpTools(MCP 发现的工具)→ assembleToolPool()(应用 deny 规则 + 去重)→ mergeAndFilterTools()(合并 initialTools 带优先级)。在添加自定义工具时,确保通过 initialTools 参数传入以获得优先级。

调试 Hook 行为:大多数 Hook 通过 logForDebugging 输出关键状态变更。使用 --debug 标志查看详细的 Hook 执行日志。对于通知 Hook,关注 addNotification 调用时机和去重 key。

架构师决策指南

React Hook vs 事件系统

Claude Code 选择 React Hook 作为主要的状态管理机制而非独立的事件总线。好处是 Hook 天然与 React 渲染周期对齐,避免了事件监听器泄漏和状态不一致。代价是 Hook 无法在 React 组件树外使用——这就是为什么 MCP 层(useManageMCPConnections)和 Hook 层之间存在 Context 桥接(MCPConnectionManager)。

手动刷新 vs 自动刷新

插件状态变更采用手动刷新(/reload-plugins)而非自动刷新。决策原因是自动刷新曾导致缓存一致性 bug:loadAllPlugins 的 memoize 被清除了,但下游的 getPluginCommandsloadPluginAgents 等仍有旧缓存。手动刷新通过 refreshActivePlugins() 一次性清除所有缓存再重新加载。

通知系统架构

通知系统由三层组成:

  1. 通知生产者(hooks/notifs/)—— 16 个 Hook 监听各种状态变更,产生通知
  2. 通知中心(context/notifications.ts)—— addNotification() 函数管理通知队列、去重、优先级
  3. 通知消费者(UI 组件)—— 渲染通知条,处理用户交互

通知有 priority(high/low)和 color(warning/suggestion/error)属性,key 用于去重。高优先级通知会立即显示,低优先级通知在空闲时显示。

并发安全:多个 Hook 可能同时调用 setAppState。React 18 的自动批处理(automatic batching)确保同一事件循环内的多个 setState 不会触发多次渲染。但 useManagePlugins 中的异步操作(如插件加载完成后写状态)在 setTimeout 回调中执行,React 18 也会批处理这些更新。

可视化处理拓扑图

REPL 组件挂载React useEffect → 初始化钩子
1. 插件生命周期初始化 — useManagePlugins()hooks/useManagePlugins.ts:37 — initialPluginLoad(L51)并行调用 loadAllPlugins()(L54)→ 依次加载各组件
加载命令getPluginCommands
加载 AgentloadPluginAgents
加载 Hooks插件钩子定义
预加载 MCPMCP 服务器配置
预加载 LSPLSP 服务器配置
每个组件独立 try-catch,错误收集到 errors 数组(不抛出)↓
更新 AppStateL148-180 — errors 去重(L154-167)+ Doctor UI
2. 工具池组装 — useMergedTools()hooks/useMergedTools.ts:20 — useMemo 缓存assembleToolPool(合并+去重+deny)→ mergeAndFilterTools(权限过滤)
内置工具
MCP 工具
initialTools
最终工具池REPL 和 Agent 共享 assembleToolPool 纯函数
stdin 键盘事件 / 工具调用事件运行时事件触发
3. 全局键盘绑定 — GlobalKeybindingHandlers()hooks/useGlobalKeybindings.tsx:36Ctrl+T 任务切换 / Ctrl+O transcript / Escape 取消
Ctrl+T任务/队友视图
Ctrl+Otranscript 模式
Escape取消操作
4. 文本输入处理 — useTextInput()hooks/useTextInput.ts:73mapKey L318 / handleEnter L247 / 历史导航 L269
5. IDE 集成 — useIDEIntegration()hooks/useIDEIntegration.tsx:15检测 IDE → initializeIdeIntegration → 文件跳转/diff/诊断
6. Shell Hook 执行引擎 — utils/hooks.tsspawn 子进程执行用户定义的 shell 命令subprocessEnv 注入会话环境变量
PreToolUse工具调用前
PostToolUse工具调用后
UserPromptSubmit用户提交提示
Notification通知事件
Stop生成停止
HookJSONOutput 验证types/hooks.ts — schema 校验
同步
阻塞等待结果
异步
后台执行不阻塞
运行时通知/会话事件工具完成 / 模型响应 / 空闲超时
7. 空闲通知 — useNotifyAfterTimeout()hooks/useNotifyAfterTimeout.ts:386 秒空闲阈值 → sendNotification 桌面通知
8. Direct Connect 会话 — useDirectConnect()hooks/useDirectConnect.ts:39 — WebSocket 管理DirectConnectSessionManager(L66)
onMessageSDK 消息 → 内部消息
追加到消息列表
onPermissionRequest权限请求处理
合成 AssistantMessage + ToolUseConfirm
防御性错误处理 — 局部失败不影响全局
hooks/useManagePlugins.ts:72-109 — 每个加载步骤独立 try-catch
① 命令加载失败 → errors.push({type, source, error})
② Agent 加载失败 → 同上,继续执行
③ Hooks 加载失败 → 同上,继续执行
④ 所有步骤都失败 → 仍返回有效结果(计数为零)
Doctor UI(/doctor 命令)精确展示每个错误来源
分层架构:底层 Shell Hook 执行引擎只知道 spawn + JSON 解析;中间层 React Hooks 将底层能力绑定到 UI 状态;顶层 REPL 在正确时机调用正确 hook。插件加载采用"收集而非抛出"的错误策略——一个损坏的插件不阻止其他插件加载,也不让 REPL 崩溃。

核心处理流程详解

Claude Code 的 hooks 系统是一个分层架构:底层是 Shell Hook 执行引擎(utils/hooks.ts),负责安全地执行用户定义的 shell 命令;中间层是 React Hooks(hooks/ 目录),负责将底层能力与 UI 状态绑定;顶层是 REPL 集成点,将 hooks 的结果注入到主循环中。整个流程围绕"插件 → 工具 → 键盘 → 通知"四个核心路径展开。

1. 插件生命周期初始化 — useManagePlugins()
hooks/useManagePlugins.ts:37 组件挂载时执行 initialPluginLoad(L51)。并行调用 loadAllPlugins()(L54)加载所有插件,然后依次:检测并卸载已下架插件(L57)、加载插件命令/Agent/Hooks(L72-109)、预加载 MCP 和 LSP 服务器配置(L120-144)、更新 AppState(L148-180)。错误被收集到 errors 数组,合并到 AppState.plugins.errors(Dedup 逻辑在 L154-167)。
2. 工具池组装 — useMergedTools()
hooks/useMergedTools.ts:20 通过 useMemo 组装完整的工具池。先调用 assembleToolPool(L30)合并内置工具和 MCP 工具,应用 deny 规则和去重。再通过 mergeAndFilterTools(L32)将 initialTools(启动时传入)和 assembled 池合并,按权限模式过滤。依赖数组包含 toolPermissionContext,权限变更时自动重计算。
3. 全局键盘绑定 — GlobalKeybindingHandlers()
hooks/useGlobalKeybindings.tsx:36 注册全局快捷键:Ctrl+T 切换任务/队友视图(L51,三态循环:none → tasks → teammates → none)、Ctrl+O 切换 transcript 模式(L95)、Escape 取消操作。使用 useKeybinding hook 统一注册,通过 useAppState 读写全局状态。Brief 模式有专门的 Escape 处理(L107-114)。
4. 文本输入处理 — useTextInput()
hooks/useTextInput.ts:73 处理所有键盘输入编辑:mapKey(L318)将按键映射到编辑操作(光标移动、删除、Kill ring);handleEnter(L247)区分 Enter 提交与 Shift+Enter 换行;历史导航(upOrHistoryUp,L269)在光标位于行首时切换历史记录;粘贴检测通过输入长度和速度判断。
5. IDE 集成 — useIDEIntegration()
hooks/useIDEIntegration.tsx:15 检测并连接 IDE:addIde(L28)在检测到 IDE 时调用 initializeIdeIntegration,设置文件跳转、diff 展示、诊断信息等。使用 useEffect 确保只初始化一次,并传递 ScopedMcpServerConfig 让 IDE 可以使用 MCP 工具。
6. 空闲通知 — useNotifyAfterTimeout()
hooks/useNotifyAfterTimeout.ts:38 实现桌面通知:首先重置交互时间戳(L49-51),然后启动轮询定时器(L53-64),每 DEFAULT_INTERACTION_THRESHOLD_MS(6 秒,L9)检查用户是否活跃。当用户空闲超过阈值时,通过 sendNotification 发送桌面通知和终端通知。交互时间由 App.tsx 的 processKeysInBatch 更新(L23-26 注释)。
7. Direct Connect 会话 — useDirectConnect()
hooks/useDirectConnect.ts:39 管理 WebSocket 会话连接。创建 DirectConnectSessionManager(L66),监听三种事件:onMessage(L67)将 SDK 消息转换为内部消息格式并追加到消息列表、onPermissionRequest(L87)创建合成 AssistantMessage 和 ToolUseConfirm 注入权限队列。tools 通过 ref 保持最新(L53-56),避免 WebSocket 回调中的闭包过期问题。
8. Shell Hook 执行 — utils/hooks.ts
utils/hooks.ts 中实现 Shell Hook 的安全执行:spawn 子进程执行用户定义的命令,通过 subprocessEnv 注入会话环境变量。支持 pre-tool-use、post-tool-use、notification 等生命周期事件。Hook 输出通过 HookJSONOutput(types/hooks.ts)schema 验证,支持同步(阻塞)和异步(后台)两种模式。
Hooks 系统的架构哲学是"分离关注点":底层的 Shell Hook 执行引擎只知道如何安全地 spawn 子进程和解析 JSON;中间层的 React Hooks 只关心如何将底层结果绑定到 UI 状态;顶层 REPL 只需要在正确的时机调用正确的 hook。这种分层让每个部分都可以独立测试和演进——例如,useManagePlugins 的错误处理逻辑(dedup、分类、Doctor UI 展示)完全不影响 useDirectConnect 的 WebSocket 管理。

设计精华

1. 插件加载的防御性错误处理

useManagePlugins(hooks/useManagePlugins.ts)的错误处理策略体现了"局部失败不影响全局"的设计原则。每个加载步骤(命令、Agent、Hooks、MCP、LSP)都有独立的 try-catch(L75-109),错误被收集到统一的 errors 数组而非抛出。即使所有步骤都失败,函数仍返回有效结果(L251-260),只是计数为零。

防御性加载
每个组件独立 try-catch,错误收集到数组而非抛出
// hooks/useManagePlugins.ts:72-109
let commands: Command[] = []
let agents: AgentDefinition[] = []

try { commands = await getPluginCommands() }
catch (error) {
  errors.push({
    type: 'generic-error', source: 'plugin-commands',
    error: `Failed to load plugin commands: ${errorMessage}`
  })
}

try { agents = await loadPluginAgents() }
catch (error) {
  errors.push({
    type: 'generic-error', source: 'plugin-agents',
    error: `Failed to load plugin agents: ${errorMessage}`
  })
}
这种"收集而非抛出"的错误策略在插件系统中至关重要。一个损坏的插件不应该阻止其他插件加载,也不应该让整个 REPL 崩溃。错误被分类存储(source 字段标识来源),Doctor UI(/doctor 命令)可以精确展示哪个插件的哪个组件出了什么问题。

2. 工具池的去重与权限过滤

useMergedTools(hooks/useMergedTools.ts:20)虽然只有 45 行,但它封装了工具系统的核心逻辑。assembleToolPool 负责合并内置工具和 MCP 工具并应用 deny 规则,mergeAndFilterTools 负责将 initialTools 合入并按权限模式过滤。两层分离确保了 REPL 和 Agent(runAgent)共享相同的工具组装逻辑。

共享工具组装函数
REPL 和 Agent 共用 assembleToolPool,确保一致的权限行为
// hooks/useMergedTools.ts:20-44
export function useMergedTools(initialTools, mcpTools, ctx): Tools {
  return useMemo(() => {
    // assembleToolPool: 共享的纯函数,REPL 和 runAgent 都用
    const assembled = assembleToolPool(ctx, mcpTools)
    // mergeAndFilterTools: 按 permission mode 过滤
    return mergeAndFilterTools(initialTools, assembled, ctx.mode)
  }, [initialTools, mcpTools, ctx])
}
将工具组装逻辑提取为纯函数而非 React Hook,是可测试性和复用性的关键决策。如果逻辑内联在 Hook 中,Agent(非 React 环境)无法复用,必须重新实现。纯函数 assembleToolPool 可以在任何上下文中调用,并通过 useMemo 在 React 中获得缓存优化。

3. 交互时间戳的集中管理

空闲检测(useNotifyAfterTimeout.ts)和交互时间跟踪采用了集中管理策略:getLastInteractionTime/updateLastInteractionTime(bootstrap/state.js)是全局唯一的交互时间源。更新点在 App.tsx 的 processKeysInBatch 中,而非 useNotifyAfterTimeout 内部的 stdin 监听器。注释(L23-26)解释了原因:额外的 stdin listener 会与主 listener 竞争,导致输入字符丢失。

集中式交互时间
全局单一时间源,避免多个 stdin listener 竞争
// hooks/useNotifyAfterTimeout.ts:23-26
// NOTE: User interaction tracking is now done in App.tsx's
// processKeysInBatch function. This avoids having a separate
// stdin 'data' listener that would compete with the main
// 'readable' listener and cause dropped input characters.

// L49-51 — 仅在 hook 挂载时重置时间戳
useEffect(() => {
  updateLastInteractionTime(true) // true = 立即重置
}, [])
多个 stdin listener 竞争是终端应用中的经典陷阱。Node.js 的 stdin 是单消费者流,'data' 事件的多个 listener 可能导致字符被随机分配给不同的 listener。集中到一个 listener 并通过全局状态共享时间戳,彻底消除了这个竞态条件。

4. WebSocket 的 ref 闭包修复

useDirectConnect(hooks/useDirectConnect.ts)面临 React Hook 的经典问题:WebSocket 回调中访问的 tools 可能过期。解决方案是使用 ref 桥接(L53-56):toolsRef 在每次渲染后同步最新的 tools,WebSocket 回调始终读取 toolsRef.current。这比重新创建 WebSocket 连接高效得多。

Ref 闭包桥接
通过 ref 保持 WebSocket 回调中的 tools 引用最新
// hooks/useDirectConnect.ts:53-56
const toolsRef = useRef(tools)
useEffect(() => {
  toolsRef.current = tools  // 每次渲染后同步
}, [tools])

// WebSocket 回调中使用 ref
const tool = findToolByName(toolsRef.current, request.tool_name)
  ?? createToolStub(request.tool_name)
这是 React Hooks 中处理长生命周期回调的标准模式。useEffect 的依赖数组决定了何时重新订阅事件,但 WebSocket 连接不应该因为 tools 变化而断开重连。Ref 提供了一个"逃生舱"——不触发重渲染,不触发重新订阅,只保证回调中读取的值是最新的。

5. 错误合并的去重策略

useManagePluginssetAppState 调用中(L148-180),新错误与现有错误(LSP 错误等)需要合并。去重逻辑(L154-167)通过构造唯一 key(type:sourcetype:source:error)避免同一错误被重复添加。这防止了插件刷新操作导致错误列表无限增长。

错误去重看似简单,但在插件系统中有一个微妙的需求:同一来源的不同错误应该保留(如两个不同的 MCP 连接失败),但同一来源的同一错误应该去重(如反复刷新导致的重复错误)。通过将 generic-error 的 key 包含错误消息而结构化错误只包含类型和来源,精确地实现了这个语义。

Agent 实践借鉴 — 客服 Agent 事件钩子设计

本节回答:如果你在设计一个客服 Agent 助手,工具调用前后需要审计日志、合规检查、异步通知,Claude Code 的 Hook 系统有哪些值得抄、哪些不需要抄?

1. 场景映射:客服 Agent 的钩子需求

客服场景有严格的合规要求。每次工具调用都需要审计、有些需要合规拦截、有些需要事后通知。这些需求恰好映射到 CC 的 Pre/Post Hook 模型:

客服场景的四种钩子需求
审计、合规、通知、脱敏
// 客服场景的钩子需求
// ──────────────────────────────────
// 1. 审计日志(Pre Hook)
//    坐席查了客户身份证号 → 必须记录:
//    "坐席张三在 2024-01-15 14:30
//     查询了客户李四的身份证号"
//
// 2. 合规检查(Pre Hook — 可 deny)
//    查询客户手机号 → 必须先记录查询理由
//    没填理由 → deny 操作
//
// 3. 异步通知(Post Hook)
//    退单操作完成 → 通知财务部门(异步不阻塞)
//
// 4. 数据脱敏(Pre Hook — modify)
//    客户手机号 138****5678
//    坐席级别不够 → modify 脱敏后返回
CC 借鉴点映射
CC 的 Hook 机制 → 客服的钩子需求
// Claude Code                 →  客服 Agent
// ────────────────────────────────────────────
Pre tool use hook       →  工具调用前的审计 + 合规
Post tool use hook      →  工具调用后的通知 + 日志
Hook 返回 allow/deny    →  合规检查可拦截操作
Hook 返回 modify        →  数据脱敏/权限过滤
防御性 try-catch        →  审计日志失败不阻塞退单
Hook 优先级排序         →  合规 hook 先于审计 hook

2. 借鉴 CC + 客服改造:伪代码实现

以下是客服 Hook 系统的核心实现,借鉴 CC 的 pre/post 生命周期、防御性错误处理和 allow/deny/modify 三种决策:

Hook 生命周期:pre → 执行 → post
借鉴 CC 的 executeToolWithHooks,适配客服场景
// 客服 Hook 生命周期 — 借鉴 CC 的三阶段执行
async function executeToolWithCSHooks(
  toolName: string,
  input: any,
  toolFn: (input: any) => Promise<any>,
  preHooks: CSHook[],
  postHooks: CSHook[]
): Promise<ToolResult> {

  // 阶段 1: Pre-hooks — 审计 + 合规检查
  let currentInput = input
  for (const hook of preHooks) {
    if (!matchPattern(hook.pattern, toolName)) continue

    const result = await safeExecuteHook(hook, toolName, currentInput)

    if (result.decision === "deny") {
      // 合规检查不通过 → 记录 deny 事件并终止
      logger.audit("TOOL_DENIED", { tool: toolName, reason: result.reason })
      return { status: "denied", reason: result.reason }
    }

    if (result.decision === "modify") {
      // 数据脱敏 → 替换输入中的敏感字段
      currentInput = result.updatedInput
    }
  }

  // 阶段 2: 执行工具
  const output = await toolFn(currentInput)

  // 阶段 3: Post-hooks — 通知 + 日志(异步,不阻塞)
  for (const hook of postHooks) {
    safeExecuteHook(hook, toolName, currentInput, output)  // fire-and-forget
  }

  return { status: "success", output }
}
Hook 返回值类型:allow / deny / modify
借鉴 CC 的 HookResult 三种决策,适配客服的合规和脱敏
// Hook 返回类型 — 借鉴 CC 的 allow/deny/modify
type CSHookResult =
  | { decision: "allow" }                     // 放行
  | { decision: "deny"; reason: string }      // 合规拦截
  | { decision: "modify"; updatedInput: any } // 数据脱敏

// Hook 1: 合规检查 — deny 无理由的敏感操作
const complianceHook: CSHook = {
  name: "compliance-check",
  pattern: /query_id_card|query_phone|query_address/,
  handler: async (toolName, input) => {
    if (!input.queryReason || input.queryReason.trim() === "") {
      return {
        decision: "deny",
        reason: `查询敏感信息(${toolName})必须填写查询理由`
      }
    }
    return { decision: "allow" }
  }
}

// Hook 2: 数据脱敏 — modify 手机号和身份证
const maskSensitiveDataHook: CSHook = {
  name: "mask-sensitive-data",
  pattern: /query_phone|query_id_card/,
  handler: async (toolName, input, agentRole) => {
    if (agentRole.level < 3) {  // 级别不够
      return {
        decision: "modify",
        updatedInput: {
          ...input,
          fieldsToMask: ["phone", "idCard"]  // 标记需要脱敏的字段
        }
      }
    }
    return { decision: "allow" }
  }
}

// Hook 3: 审计日志 — 记录所有敏感操作
const auditLogHook: CSHook = {
  name: "audit-log",
  pattern: /.*/,  // 匹配所有工具
  handler: async (toolName, input) => {
    await auditLog.write({
      operator: input.operatorName,
      action: toolName,
      customerId: input.customerId,
      timestamp: new Date(),
      queryReason: input.queryReason
    })
    return { decision: "allow" }
  }
}
防御性执行 + Hook 注册和优先级
单个 hook 失败不影响主流程,优先级保证合规先于审计
// 防御性 Hook 执行器 — 借鉴 CC 的独立 try-catch
async function safeExecuteHook(
  hook: CSHook, toolName: string, ...args: any[]
): Promise<CSHookResult> {
  try {
    return await hook.handler(toolName, ...args)
  } catch (error) {
    // 审计日志写入失败不能阻塞退单操作
    logger.error(`Hook "${hook.name}" failed`, { error: error.message })
    return { decision: "allow" }  // 失败时放行,不阻塞主流程
  }
}

// Hook 注册表 — 按优先级排序
class CSHookRegistry {
  private preHooks: RegisteredHook[] = []

  register(name: string, pattern: RegExp, handler: HookHandler, priority: number = 100) {
    this.preHooks.push({ name, pattern, handler, priority })
    this.preHooks.sort((a, b) => a.priority - b.priority)  // 数字小先执行
  }
}

// 注册 — 优先级保证执行顺序
const registry = new CSHookRegistry()
registry.register("compliance-check", /query_/, complianceHandler, 10)    // 最先:合规
registry.register("mask-sensitive-data", /query_/, maskHandler, 20)       // 其次:脱敏
registry.register("audit-log", /.*/, auditHandler, 50)                    // 最后:审计

3. 落地清单:必须抄 vs 不需要抄

必须抄:
  • Pre/Post 生命周期 — 工具调用前做审计和合规检查(pre),调用后做通知和日志(post)。CC 的 pre→execute→post 模型完美适配
  • 防御性错误处理 — 每个 hook 独立 try-catch,审计日志写入失败不能阻塞退单操作。CC 的 safeExecute 就是这个模式
  • Hook 可 deny 操作 — 合规 hook 返回 deny 可以阻止敏感操作。CC 的 allow/deny/modify 三种决策就是为这种场景设计的
  • Hook 优先级排序 — 合规 hook(priority=10)必须在审计 hook(priority=50)之前执行,确保不合规的请求不会被记录为"已执行"
不需要抄:
  • React Hooks — CC 代码中大量 useXxx 是 React 状态管理 hooks,和本节的"事件钩子"完全不是一回事。客服面板用标准 Web 框架的 hooks 就行
  • 50+ 自定义 hooks — CC 有 useTextInput、useGlobalKeybindings 等 50+ hooks。客服场景 5-10 个 hook(审计、合规、通知、脱敏、限流)就够
  • Hook 可修改工具输入路径 — CC 的 modify 可以改变工具调用的输入参数。客服场景主要用 deny 拦截和 modify 脱敏,不需要复杂参数变换

4. 常见坑

坑 1:审计 hook 抛异常导致退单操作失败。审计日志服务的数据库连接超时,异常向上传播,坐席的退单操作直接报错。解决方案:借鉴 CC 的防御性执行——每个 hook 独立 try-catch,失败记录日志并返回 allow,绝不阻塞主流程。
坑 2:合规 hook 和审计 hook 执行顺序颠倒。审计 hook 先执行记录了操作日志,然后合规 hook deny 了操作——日志里记了一条"已执行"但实际没执行。解决方案:通过显式优先级确保合规 hook(priority=10)永远在审计 hook(priority=50)之前。
坑 3:Post hook 同步 await 导致坐席等待。退单操作本身 200ms 完成,但 post hook 里的"通知财务"发了邮件等了 3 秒,坐席等了 3.2 秒才看到结果。解决方案:Post hook 用 fire-and-forget 模式(不 await),或者放消息队列异步处理。
坑 4:Hook 注册太分散,不知道系统里有哪些 hook。有人在 controller 里直接 if-else 做合规检查,有人用 hook 系统注册,两套逻辑互相覆盖。解决方案:所有拦截逻辑必须通过 HookRegistry 注册,禁止在业务代码中直接写 if-else 拦截。

代码索引

文件行数说明
hooks/useManagePlugins.ts~305插件管理 Hook:初始化加载、错误收集、刷新通知
hooks/useMergedTools.ts~45工具合并 Hook:assembleToolPool + mergeAndFilterTools
hooks/useNotifyAfterTimeout.ts~66空闲通知 Hook:idle 检测 + 桌面/终端通知
hooks/useGlobalKeybindings.tsx~大型全局键盘绑定:Escape/Ctrl+C/自定义快捷键
hooks/useTextInput.ts~大型文本输入处理:stdin 读取、多行编辑、粘贴
hooks/useIDEIntegration.tsx~大型IDE 集成:文件跳转、diff 展示、诊断
hooks/useDirectConnect.ts~中型Direct Connect:WebSocket 会话连接管理
hooks/useSSHSession.ts~中型SSH 会话:远程连接状态管理
hooks/useRemoteSession.ts~中型远程会话:Teleport 协议
hooks/useQueueProcessor.ts~中型命令队列处理:异步命令调度
hooks/useTerminalSize.ts~小型终端尺寸:resize 事件订阅
hooks/useElapsedTime.ts~小型计时器:会话/请求耗时显示
hooks/useAfterFirstRender.ts~小型首帧回调:跳过首次渲染的副作用
hooks/useTimeout.ts~小型定时器 Hook:可取消的 setTimeout
hooks/useDoublePress.ts~小型双击检测:Esc 双击退出确认
hooks/toolPermission/interactiveHandler.ts~大型交互式权限决策:用户确认对话框
hooks/toolPermission/coordinatorHandler.ts~中型协调器权限决策:Swarm 模式
hooks/toolPermission/swarmWorkerHandler.ts~中型Worker 权限决策:Agent 模式
hooks/toolPermission/PermissionContext.ts~中型权限上下文:决策链初始化
hooks/notifs/useMcpConnectivityStatus.tsx~中型MCP 连接状态通知
hooks/notifs/usePluginAutoupdateNotification.ts~中型插件自动更新通知
hooks/notifs/useRateLimitWarningNotification.tsx~中型速率限制警告通知
hooks/notifs/useStartupNotification.ts~小型启动消息通知
hooks/notifs/useSettingsErrors.tsx~小型设置错误通知