Codex 系统手册

Hook 执行和停止-继续协调

stage-14.1.316 个文件

这一阶段像系统里的“门卫和传话员”,贯穿启动、运行到收尾。Hook 就是在关键时刻自动跑的小脚本。注册表和运行时先找到配置好的脚本,调度器决定哪个事件该叫谁。命令运行器负责真正执行,解析器读懂脚本吐出的 JSON,输出太长就先存临时文件。各事件处理器分别管会话开始、用户发话、工具前后、权限申请、上下文压缩和停止时的检查,最后汇总成放行、拦住、停止或补充上下文。

本阶段的文件16

注册表与运行时入口

这些文件公开公共钩子 API,并将会话级运行时流程连接到钩子引擎和旧版通知路径。

hooks/src/registry.rs源码 ↗
orchestrationstartup, request handling, cross-cutting

这个文件把各种来源的钩子统一装起来,并在合适的时候把请求交给真正的钩子执行引擎。它像一个前台接待:启动时根据配置创建 Hooks,收集旧版通知命令、插件钩子、信任设置、shell 命令等;运行时别人不需要知道钩子藏在哪里,只要调用 preview_ 先看看会跑哪些钩子,或者调用 run_ 真正执行。文件里还保留了一套旧的 AfterAgent 钩子分发方式:dispatch 会按顺序执行,如果某个钩子说“停下”,后面的就不再跑。list_hooks 用来列出当前能发现的钩子,方便界面或诊断工具展示。command_from_argv 则把一串命令行文字变成可执行命令,避免每处代码都重复拆参数。

函数细节23
Hooks::default54–56 ↗
fn default() -> Self

作用:创建一个最朴素的 Hooks 对象,使用全空的默认配置。有人只想要一个能用但不额外开启功能的钩子入口时,会走这里。

数据流:进去没有额外输入 → 它先生成默认的 HooksConfig,再交给 Hooks::new 统一建对象 → 出来一个 Hooks,里面按默认设置初始化好。

调用关系:它是 Hooks 的默认构造方式,本身不做复杂判断,而是把活交给 Hooks::new,保证默认创建和带配置创建走同一套流程。

调用图:外部调用 2 个(new, default)。

Hooks::new60–82 ↗
fn new(config: HooksConfig) -> Self

作用:根据配置创建整个钩子注册表,也就是之后所有钩子预览和执行的入口。它把旧版通知钩子和新版引擎都准备好。

数据流:进去一个 HooksConfig,里面有是否启用、插件来源、信任开关、shell 设置、旧版通知命令等 → 它把有效的 legacy_notify_argv 转成 AfterAgent 钩子,并创建 ClaudeHooksEngine → 出来一个 Hooks,保存旧版钩子列表和新版执行引擎。

调用关系:这是这个文件最核心的组装函数,会被配置构建、会话创建、权限钩子安装和测试场景调用。它内部调用 ClaudeHooksEngine::new,把真正发现和执行钩子的工作交给引擎。

调用图:调用 1 个内部函数(new);被 6 处调用(install_mcp_permission_request_hook, build_hooks_for_config, make_session_and_context, make_session_and_context_with_auth_config_home_and_rx, preview_session_start_hooks, execve_permission_request_hook_short_circuits_prompt)。

Hooks::startup_warnings84–86 ↗
fn startup_warnings(&self) -> &[String]

作用:取出启动阶段发现的钩子警告信息。比如插件没加载好、配置有问题时,上层可以把这些消息展示给用户。

数据流:进去一个已经创建好的 Hooks → 它读取内部 engine 保存的 warnings → 出来一组字符串警告,不修改任何状态。

调用关系:它是外界查看启动问题的小窗口,自己不生成警告,只是转交 ClaudeHooksEngine 已经收集好的 warnings。

调用图:调用 1 个内部函数(warnings)。

Hooks::hooks_for_event88–92 ↗
fn hooks_for_event(&self, hook_event: &HookEvent) -> &[Hook]

作用:根据当前发生的事件,找出应该运行的旧版钩子列表。现在它只处理 AfterAgent 这一类旧事件。

数据流:进去一个 HookEvent → 它匹配事件类型 → 如果是 AfterAgent,就返回 Hooks 里保存的 after_agent 钩子切片。

调用关系:它被 Hooks::dispatch 调用,是旧版 dispatch 流程里的“查表”步骤。dispatch 不自己判断事件对应哪些钩子,而是先问它。

调用图:被 1 处调用(dispatch)。

Hooks::dispatch94–107 ↗
async fn dispatch(&self, hook_payload: HookPayload) -> Vec<HookResponse>

作用:按顺序执行旧版钩子,并在某个钩子要求中止时立刻停下。这样可以避免危险操作继续往后走。

数据流:进去一个 HookPayload,里面带着事件和上下文 → 它先用 Hooks::hooks_for_event 找到对应钩子,再逐个 await 执行,收集每个结果;如果某个结果说应该停止操作,就提前跳出 → 出来一组 HookResponse,记录已经跑过的钩子结果。

调用关系:这是旧版 AfterAgent 钩子的实际分发器。它调用 hooks_for_event 选钩子,再调用每个 Hook 的 execute;相比新版 run_* 方法,它处理的是保存在 after_agent 里的旧式钩子。

调用图:调用 1 个内部函数(hooks_for_event);外部调用 1 个(with_capacity)。

Hooks::preview_session_start109–114 ↗
fn preview_session_start(
        &self,
        request: &SessionStartRequest,
    ) -> Vec<codex_protocol::protocol::HookRunSummary>

作用:预览“会话开始”时会运行哪些钩子,但不真正执行。适合界面提前展示,或者做调试说明。

数据流:进去一个 SessionStartRequest 引用 → 它直接交给内部 engine 的 preview_session_start → 出来一组 HookRunSummary,说明将要运行的钩子概况。

调用关系:它是 Hooks 对外提供的薄包装,真正判断和汇总由 ClaudeHooksEngine 完成。上层想看会话开始钩子时会调用它。

调用图:调用 1 个内部函数(preview_session_start)。

Hooks::preview_pre_tool_use116–121 ↗
fn preview_pre_tool_use(
        &self,
        request: &PreToolUseRequest,
    ) -> Vec<codex_protocol::protocol::HookRunSummary>

作用:预览“工具使用前”会触发哪些钩子,不执行它们。这样用户或系统能先知道哪些检查会发生。

数据流:进去一个 PreToolUseRequest 引用 → 它把请求传给 engine.preview_pre_tool_use → 出来一组 HookRunSummary,描述匹配到的钩子。

调用关系:它处在上层调用者和 ClaudeHooksEngine 之间,只负责转发。真正的匹配规则在引擎里。

调用图:调用 1 个内部函数(preview_pre_tool_use)。

Hooks::preview_permission_request123–128 ↗
fn preview_permission_request(
        &self,
        request: &PermissionRequestRequest,
    ) -> Vec<codex_protocol::protocol::HookRunSummary>

作用:预览“请求权限”时会跑哪些钩子。权限请求通常关系到是否允许执行某些操作,所以提前看清楚很有用。

数据流:进去一个 PermissionRequestRequest 引用 → 它交给 engine.preview_permission_request → 出来一组钩子运行摘要。

调用关系:它是权限请求预览的门面函数。上层不用直接接触 engine,只通过 Hooks 调用。

调用图:调用 1 个内部函数(preview_permission_request)。

Hooks::preview_post_tool_use130–135 ↗
fn preview_post_tool_use(
        &self,
        request: &PostToolUseRequest,
    ) -> Vec<codex_protocol::protocol::HookRunSummary>

作用:预览“工具使用后”会运行哪些钩子。工具已经跑完后,钩子可能做记录、检查结果或后续处理。

数据流:进去一个 PostToolUseRequest 引用 → 它转给 engine.preview_post_tool_use → 出来 HookRunSummary 列表。

调用关系:它把外部请求接到 ClaudeHooksEngine 的对应预览能力上,本身不分析钩子内容。

调用图:调用 1 个内部函数(preview_post_tool_use)。

Hooks::run_session_start137–143 ↗
async fn run_session_start(
        &self,
        request: SessionStartRequest,
        turn_id: Option<String>,
    ) -> SessionStartOutcome

作用:真正执行“会话开始”钩子。会话刚启动时,系统可以借此做初始化、提示、检查或记录。

数据流:进去一个 SessionStartRequest 和可选的 turn_id → 它异步调用 engine.run_session_start → 出来 SessionStartOutcome,表示钩子执行后的结果和影响。

调用关系:这是会话开始阶段的实际执行入口。它把工作交给 ClaudeHooksEngine,并等待引擎返回最终结果。

调用图:调用 1 个内部函数(run_session_start)。

Hooks::run_pre_tool_use145–147 ↗
async fn run_pre_tool_use(&self, request: PreToolUseRequest) -> PreToolUseOutcome

作用:真正执行“工具使用前”钩子。这个阶段很关键,因为钩子可以在工具运行前检查或阻止不合适的操作。

数据流:进去一个 PreToolUseRequest → 它异步交给 engine.run_pre_tool_use → 出来 PreToolUseOutcome,说明允许、拒绝或其他处理结果。

调用关系:它是工具执行前的安全检查入口之一。上层准备使用工具时调用它,真正执行细节由 engine 完成。

调用图:调用 1 个内部函数(run_pre_tool_use)。

Hooks::run_permission_request149–154 ↗
async fn run_permission_request(
        &self,
        request: PermissionRequestRequest,
    ) -> PermissionRequestOutcome

作用:真正执行“权限请求”钩子,用来决定或影响某个需要授权的动作。比如某个命令要访问敏感资源时,可以先经过这里。

数据流:进去一个 PermissionRequestRequest → 它异步调用 engine.run_permission_request → 出来 PermissionRequestOutcome,表达钩子对权限请求的处理结果。

调用关系:它连接权限流程和钩子引擎。调用方只关心最终权限结果,具体哪些钩子参与由 engine 负责。

调用图:调用 1 个内部函数(run_permission_request)。

Hooks::run_post_tool_use156–158 ↗
async fn run_post_tool_use(&self, request: PostToolUseRequest) -> PostToolUseOutcome

作用:真正执行“工具使用后”钩子。工具跑完后,这些钩子可以查看结果并做收尾动作。

数据流:进去一个 PostToolUseRequest → 它异步交给 engine.run_post_tool_use → 出来 PostToolUseOutcome,包含工具后置钩子的执行结果。

调用关系:它是工具执行后的钩子入口,作为 Hooks 的公开方法,把实际工作委托给 ClaudeHooksEngine。

调用图:调用 1 个内部函数(run_post_tool_use)。

Hooks::preview_pre_compact160–165 ↗
fn preview_pre_compact(
        &self,
        request: &PreCompactRequest,
    ) -> Vec<codex_protocol::protocol::HookRunSummary>

作用:预览“压缩上下文前”会运行哪些钩子。压缩上下文可以理解成把聊天或运行信息变短,钩子可在这之前检查或准备。

数据流:进去一个 PreCompactRequest 引用 → 它调用 engine.preview_pre_compact → 出来一组 HookRunSummary。

调用关系:它服务于上下文压缩前的展示或诊断场景,实际发现匹配钩子的逻辑由 engine 处理。

调用图:调用 1 个内部函数(preview_pre_compact)。

Hooks::run_pre_compact167–169 ↗
async fn run_pre_compact(&self, request: PreCompactRequest) -> PreCompactOutcome

作用:真正执行“压缩上下文前”钩子。系统在删减或整理上下文之前,可以让钩子先决定是否继续或做准备。

数据流:进去一个 PreCompactRequest → 它异步调用 engine.run_pre_compact → 出来 PreCompactOutcome,表示钩子执行后的决定或结果。

调用关系:它是上下文压缩流程前半段的钩子执行入口,上层流程调用它,引擎负责具体运行。

调用图:调用 1 个内部函数(run_pre_compact)。

Hooks::preview_post_compact171–176 ↗
fn preview_post_compact(
        &self,
        request: &PostCompactRequest,
    ) -> Vec<codex_protocol::protocol::HookRunSummary>

作用:预览“压缩上下文后”会运行哪些钩子。压缩完成后可能需要记录、通知或检查结果。

数据流:进去一个 PostCompactRequest 引用 → 它转给 engine.preview_post_compact → 出来 HookRunSummary 列表。

调用关系:它是压缩后钩子的预览门面。调用者通过它了解将要发生什么,不直接操作引擎细节。

调用图:调用 1 个内部函数(preview_post_compact)。

Hooks::run_post_compact178–180 ↗
async fn run_post_compact(&self, request: PostCompactRequest) -> StatelessHookOutcome

作用:真正执行“压缩上下文后”钩子。因为这类钩子不需要保留复杂状态,所以返回的是无状态结果。

数据流:进去一个 PostCompactRequest → 它异步调用 engine.run_post_compact → 出来 StatelessHookOutcome,说明这些后置钩子的运行结果。

调用关系:它位于上下文压缩完成之后,由上层压缩流程调用,具体执行交给 ClaudeHooksEngine。

调用图:调用 1 个内部函数(run_post_compact)。

Hooks::preview_user_prompt_submit182–187 ↗
fn preview_user_prompt_submit(
        &self,
        request: &UserPromptSubmitRequest,
    ) -> Vec<codex_protocol::protocol::HookRunSummary>

作用:预览“用户提交提示词”时会运行哪些钩子。用户输入真正送进去前,系统可以先展示会有哪些检查或处理。

数据流:进去一个 UserPromptSubmitRequest 引用 → 它调用 engine.preview_user_prompt_submit → 出来一组 HookRunSummary。

调用关系:它是用户输入提交阶段的预览入口,负责把请求转给 engine,而不是自己判断规则。

调用图:调用 1 个内部函数(preview_user_prompt_submit)。

Hooks::run_user_prompt_submit189–194 ↗
async fn run_user_prompt_submit(
        &self,
        request: UserPromptSubmitRequest,
    ) -> UserPromptSubmitOutcome

作用:真正执行“用户提交提示词”钩子。这个阶段可以检查用户输入、改写、拒绝或附加处理。

数据流:进去一个 UserPromptSubmitRequest → 它异步调用 engine.run_user_prompt_submit → 出来 UserPromptSubmitOutcome,表示钩子对这次用户输入的处理结果。

调用关系:它连接用户输入流程和钩子引擎。上层在接受用户提示词时调用它,引擎负责运行匹配到的钩子。

调用图:调用 1 个内部函数(run_user_prompt_submit)。

Hooks::preview_stop196–201 ↗
fn preview_stop(
        &self,
        request: &StopRequest,
    ) -> Vec<codex_protocol::protocol::HookRunSummary>

作用:预览“停止”事件会运行哪些钩子。停止可能是会话结束或流程中断前的收尾时刻。

数据流:进去一个 StopRequest 引用 → 它调用 engine.preview_stop → 出来 HookRunSummary 列表。

调用关系:它是停止阶段的预览入口,给上层看清楚结束前可能触发的钩子。

调用图:调用 1 个内部函数(preview_stop)。

Hooks::run_stop203–205 ↗
async fn run_stop(&self, request: StopRequest) -> StopOutcome

作用:真正执行“停止”事件钩子。系统准备停止时,可以通过它做清理、通知或最后检查。

数据流:进去一个 StopRequest → 它异步调用 engine.run_stop → 出来 StopOutcome,说明停止钩子的执行结果。

调用关系:它是停止流程中的实际执行入口,调用者触发停止时用它,具体运行由 ClaudeHooksEngine 承担。

调用图:调用 1 个内部函数(run_stop)。

list_hooks208–223 ↗
fn list_hooks(config: HooksConfig) -> HookListOutcome

作用:列出当前配置下能发现的钩子,并带上发现过程中产生的警告。它适合给命令行、界面或诊断工具展示“系统现在有哪些钩子”。

数据流:进去一个 HooksConfig → 如果钩子功能没启用,它直接返回空的 HookListOutcome;如果启用了,它调用 discover_handlers 去扫描配置层和插件来源 → 出来 HookListOutcome,里面有钩子条目和警告信息。

调用关系:它不创建 Hooks,也不执行钩子,只做发现和汇总。它调用 engine::discovery::discover_handlers,把真正扫描配置和插件的工作交给发现模块。

调用图:调用 1 个内部函数(discover_handlers);外部调用 1 个(default)。

command_from_argv225–233 ↗
fn command_from_argv(argv: &[String]) -> Option<Command>

作用:把一组命令行字符串变成 tokio 可异步执行的 Command。简单说,就是把“程序名 + 参数列表”包装成系统能启动的命令。

数据流:进去一个字符串数组切片 argv → 它取第一个元素当程序名,后面的当参数;如果数组为空或程序名为空,就返回 None;否则创建 Command 并放入参数 → 出来 Some(Command)。

调用关系:这是一个小工具函数,用来避免各处重复解析命令行参数。它调用 Command::new 创建命令对象,通常会被需要从 argv 启动外部程序的代码使用。

调用图:外部调用 1 个(new)。

core/src/hook_runtime.rs源码 ↗
orchestrationcross-cutting;贯穿会话开始、每轮输入、工具调用、权限请求、上下文压缩和回合结束

这个文件解决的问题是:主流程不能直接到处散落着“要不要跑钩子、怎么通知前端、怎么记录结果、要不要停止”的代码,否则很容易漏记日志、漏发事件,或者在子代理场景下跑错钩子。它像一个门岗和值班记录员:每到一个关键节点,就组装一份请求,把会话编号、当前目录、模型名、权限模式、工具输入等信息交给钩子系统;钩子开始和结束时,它会发事件给外部界面,同时写指标和分析数据;如果钩子返回了额外上下文,它会把这些内容记录进对话,让模型后续能看到。它还区分普通会话和“子代理”(由主任务派生出来的内部小助手),避免内部任务误触发用户配置的生命周期钩子。

函数细节31
ContextInjectingHookOutcome::from85–99 ↗
fn from(value: UserPromptSubmitOutcome) -> Self

作用:把某些钩子的原始结果,转换成这个文件统一能处理的格式。这样会话开始钩子和用户提交钩子虽然类型不同,但后面都能按同一套流程处理。

数据流:进去的是一个钩子运行结果,里面有完成事件、是否停止、额外上下文等信息 → 它丢掉这里不需要的停止原因,把剩下的信息装进统一的 ContextInjectingHookOutcome → 出来的是一份标准结果,方便后续发完成事件和记录额外上下文。

调用关系:它服务于 run_context_injecting_hook。run_context_injecting_hook 不关心具体是哪类钩子,只要求结果能转成这个统一形状。

run_pending_session_start_hooks103–156 ↗
async fn run_pending_session_start_hooks(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
) -> bool

作用:在会话刚开始、或者特定子代理启动时,运行还没执行过的启动钩子。它决定这个会话是否应该因为钩子的要求而停止。

数据流:进去的是当前会话和当前回合上下文 → 它从会话里取出待运行的启动来源,判断这是普通会话启动还是线程派生的子代理启动,组装包含会话号、目录、模型、权限模式等信息的请求 → 它运行匹配的启动钩子,记录钩子给出的额外上下文,最后返回是否需要停止。

调用关系:它由 run_turn 在一轮运行早期调用。它会用 hook_permission_mode 翻译权限模式,用 subagent_hook_context 准备子代理信息,并把实际的“发开始事件、等结果、发完成事件”交给 run_context_injecting_hook。

调用图:调用 3 个内部函数(hook_permission_mode, run_context_injecting_hook, subagent_hook_context);被 1 处调用(run_turn);外部调用 1 个(matches!)。

run_pre_tool_use_hooks163–220 ↗
async fn run_pre_tool_use_hooks(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    tool_use_id: String,
    tool_name: &HookToolName,
    tool_input: &Value,
) -> PreToolUseHookResult

作用:在工具真正执行前运行 PreToolUse 钩子,让外部规则有机会修改工具输入,或者直接拦住危险操作。

数据流:进去的是会话、回合、工具调用编号、工具名和工具输入 → 它组装一份给钩子的请求,先发“钩子开始”事件,再等待钩子结果 → 如果钩子允许继续,就返回可能被改过的输入;如果钩子要求阻止,就返回一段清楚的拦截原因,并在 Bash 或 apply_patch 这类命令工具上附带被拦的命令。

调用关系:它由工具分发流程 dispatch_any_with_terminal_outcome 在执行工具前调用。它会调用 emit_hook_started_events 和 emit_hook_completed_events 通知外部界面,也会调用 record_additional_contexts 把钩子补充的信息写入对话。

调用图:调用 7 个内部函数(emit_hook_completed_events, emit_hook_started_events, hook_permission_mode, record_additional_contexts, thread_spawn_subagent_hook_context, matcher_aliases, name);被 1 处调用(dispatch_any_with_terminal_outcome);外部调用 4 个(clone, get, Blocked, format!)。

run_permission_request_hooks225–256 ↗
async fn run_permission_request_hooks(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    run_id_suffix: &str,
    payload: PermissionRequestPayload,
) -> Option<PermissionRequestDecisi

作用:在系统准备向用户申请权限时,先让权限请求钩子判断是否可以自动给出决定。它可以减少不必要的人工确认,也能套上组织或项目规则。

数据流:进去的是会话、回合、一次请求的后缀编号,以及权限请求里包含的工具名和输入 → 它把这些信息包装成 PermissionRequestRequest,发出钩子开始事件,运行钩子 → 出来的是可选的权限决定;如果钩子没给决定,就返回空,让原来的审批流程继续。

调用关系:它会被 maybe_request_mcp_tool_approval、handle_inline_policy_request、request_approval 和 prompt 等需要权限判断的地方调用。它沿用标准的开始事件和完成事件流程,但不修改工具输入。

调用图:调用 4 个内部函数(emit_hook_completed_events, emit_hook_started_events, hook_permission_mode, thread_spawn_subagent_hook_context);被 4 处调用(maybe_request_mcp_tool_approval, handle_inline_policy_request, request_approval, prompt)。

run_post_tool_use_hooks264–295 ↗
async fn run_post_tool_use_hooks(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    tool_use_id: String,
    tool_name: String,
    matcher_aliases: Vec<String>,
    tool_input: Value,

作用:在工具成功执行后运行 PostToolUse 钩子,让外部脚本看到工具输入和输出,做记录、检查或后续处理。

数据流:进去的是会话、回合、工具编号、工具名、匹配别名、工具输入和工具输出 → 它组装成稳定格式的请求,发出开始事件,运行钩子 → 出来的是 PostToolUseOutcome,其中包含钩子完成事件和可能的后续信息;同时它已经把完成事件发出去了。

调用关系:它由 dispatch_any_with_terminal_outcome 在工具成功返回后调用。调用方负责把工具数据先整理成钩子约定的稳定格式,避免把内部实现细节泄露给用户钩子。

调用图:调用 4 个内部函数(emit_hook_completed_events, emit_hook_started_events, hook_permission_mode, thread_spawn_subagent_hook_context);被 1 处调用(dispatch_any_with_terminal_outcome)。

run_turn_stop_hooks298–366 ↗
async fn run_turn_stop_hooks(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    stop_hook_active: bool,
    last_assistant_message: Option<String>,
) -> StopOutcome

作用:在一轮对话准备结束时运行 Stop 或 SubagentStop 钩子,让外部规则有机会在收尾阶段执行检查、清理或要求继续停止流程。

数据流:进去的是会话、回合、当前是否已有停止钩子在运行,以及最后一条助手消息 → 它先判断当前是普通会话还是线程派生的子代理;如果是子代理,还会尝试找到父线程的记录路径 → 然后组装停止请求,发开始事件,运行钩子,发完成事件,最后返回钩子的 StopOutcome。

调用关系:它由 run_turn 在回合结束阶段调用。它会用 subagent_hook_context 准备子代理身份,用 hook_permission_mode 写入权限模式,并用 emit_hook_started_events、emit_hook_completed_events 走统一事件流程。

调用图:调用 4 个内部函数(emit_hook_completed_events, emit_hook_started_events, hook_permission_mode, subagent_hook_context);被 1 处调用(run_turn);外部调用 3 个(default, take, warn!)。

run_pre_compact_hooks368–393 ↗
async fn run_pre_compact_hooks(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    trigger: CompactionTrigger,
) -> PreCompactHookOutcome

作用:在系统压缩上下文之前运行 PreCompact 钩子。上下文压缩会改变模型接下来能看到的历史内容,所以这里给外部规则一个提前介入的机会。

数据流:进去的是会话、回合和压缩触发原因,比如手动或自动 → 它把触发原因转成钩子能读懂的文字,组装请求,发开始事件并运行钩子 → 如果钩子要求停止,就返回 Stopped;否则返回 Continue。

调用关系:它由本地和远程的压缩任务内部流程调用。它依赖 compaction_trigger_label 标准化触发原因,并使用标准的钩子开始、完成事件函数。

调用图:调用 4 个内部函数(compaction_trigger_label, emit_hook_completed_events, emit_hook_started_events, thread_spawn_subagent_hook_context);被 3 处调用(run_compact_task_inner, run_remote_compact_task_inner, run_remote_compact_task_inner)。

run_post_compact_hooks405–430 ↗
async fn run_post_compact_hooks(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    trigger: CompactionTrigger,
) -> PostCompactHookOutcome

作用:在系统完成上下文压缩之后运行 PostCompact 钩子。这样外部脚本可以知道压缩已经发生,并决定是否让后续流程继续。

数据流:进去的是会话、回合和压缩触发原因 → 它组装压缩后的钩子请求,发开始事件,运行钩子并发完成事件 → 如果钩子要求停止,返回 Stopped;否则返回 Continue。

调用关系:它由本地和远程压缩任务在压缩完成后调用。它和 run_pre_compact_hooks 是一前一后的配套流程。

调用图:调用 4 个内部函数(compaction_trigger_label, emit_hook_completed_events, emit_hook_started_events, thread_spawn_subagent_hook_context);被 3 处调用(run_compact_task_inner, run_remote_compact_task_inner, run_remote_compact_task_inner)。

run_legacy_after_agent_hook433–498 ↗
async fn run_legacy_after_agent_hook(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    input: &[ResponseItem],
    last_assistant_message: Option<String>,
) -> bool

作用:运行旧版 after_agent 钩子,也就是助手完成一轮回复之后的老式扩展点。它主要是为了兼容过去已有的配置。

数据流:进去的是会话、回合、本轮输入项目和最后一条助手消息 → 它从输入里提取用户消息,构造旧版钩子载荷并逐个执行 → 如果某个钩子失败但允许继续,它只记警告;如果失败并要求中止,它向会话发送错误事件,并返回 true 表示本轮收尾被中止。

调用关系:它由 run_turn 在回合结束附近调用。它不走新版 HookStarted 和 HookCompleted 的事件模型,而是使用旧的 dispatch 机制。

调用图:被 1 处调用(run_turn);外部调用 5 个(now, format!, iter, Error, warn!)。

inspect_pending_input500–537 ↗
async fn inspect_pending_input(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    pending_input_item: &TurnInput,
) -> HookRuntimeOutcome

作用:在把用户输入真正写进会话历史前,先检查这份输入是否会触发 UserPromptSubmit 钩子。它可以让钩子在输入进入对话前补充上下文,或者要求停止。

数据流:进去的是会话、回合和一条待记录输入 → 如果这是用户输入,它组装包含提示词、模型、权限模式等信息的请求并运行钩子;如果是响应项目或代理间通信,就直接返回“不停止、无额外上下文” → 出来的是 HookRuntimeOutcome,告诉调用方是否停止以及有哪些额外上下文要记录。

调用关系:它由 run_hooks_and_record_inputs 和 on_task_finished 在处理待写入输入时调用。用户输入场景会把实际运行交给 run_context_injecting_hook。

调用图:调用 4 个内部函数(hook_permission_mode, run_context_injecting_hook, thread_spawn_subagent_hook_context, new);被 2 处调用(run_hooks_and_record_inputs, on_task_finished);外部调用 1 个(new)。

record_pending_input539–564 ↗
async fn record_pending_input(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    pending_input: TurnInput,
    additional_contexts: Vec<String>,
)

作用:把已经通过钩子检查的待输入,正式写进会话记录里。它同时把钩子补充的额外上下文也写进去。

数据流:进去的是会话、回合、一条待记录输入和额外上下文列表 → 它按输入类型分别记录:用户输入写成用户消息,响应项目写成对话项目,代理间通信写成通信记录 → 最后调用 record_additional_contexts,把钩子补充内容也加入历史。

调用关系:它由 run_hooks_and_record_inputs 和 on_task_finished 调用,通常跟 inspect_pending_input 前后配合:先检查,再记录。

调用图:调用 1 个内部函数(record_additional_contexts);被 2 处调用(run_hooks_and_record_inputs, on_task_finished);外部调用 1 个(from_ref)。

run_context_injecting_hook566–581 ↗
async fn run_context_injecting_hook(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    preview_runs: Vec<HookRunSummary>,
    outcome_future: Fut,
) -> HookRuntimeOutcome

作用:封装一类会“往对话里补充上下文”的钩子的通用运行流程。它保证开始事件、等待结果、完成事件这三步不会漏掉。

数据流:进去的是会话、回合、预览出来的钩子运行摘要,以及一个真正执行钩子的异步任务 → 它先把所有预览运行发成 HookStarted 事件,再等待钩子完成,把结果转成统一格式,发送 HookCompleted 事件 → 出来的是统一的 HookRuntimeOutcome。

调用关系:它被 inspect_pending_input 和 run_pending_session_start_hooks 使用。具体钩子怎么跑由传进来的异步任务负责,它只管公共外壳流程。

调用图:调用 2 个内部函数(emit_hook_completed_events, emit_hook_started_events);被 2 处调用(inspect_pending_input, run_pending_session_start_hooks)。

HookRuntimeOutcome::record_additional_contexts584–592 ↗
async fn record_additional_contexts(
        self,
        sess: &Arc<Session>,
        turn_context: &Arc<TurnContext>,
    ) -> bool

作用:把 HookRuntimeOutcome 里的额外上下文写入会话,并顺手返回它原本的“是否停止”标记。这样调用方可以一行代码完成记录和判断。

数据流:进去的是一个 HookRuntimeOutcome、会话和回合 → 它取出 additional_contexts,交给 record_additional_contexts 写入对话 → 出来的是 should_stop 布尔值,表示流程是否该停。

调用关系:它常接在 run_context_injecting_hook 之后使用,尤其是 run_pending_session_start_hooks 里,用来把钩子补充信息落盘并决定是否停止。

调用图:调用 1 个内部函数(record_additional_contexts)。

record_additional_contexts595–607 ↗
async fn record_additional_contexts(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    additional_contexts: Vec<String>,
)

作用:把钩子返回的额外说明文字,变成模型后续能看到的开发者消息并写入会话历史。

数据流:进去的是会话、回合和多段额外上下文字符串 → 它先用 additional_context_messages 把字符串转换成对话项目;如果为空就什么也不做 → 如果有内容,就调用会话记录接口,把这些开发者消息加入对话。

调用关系:它被多个流程调用,包括工具前钩子、输入记录、任务完成和 HookRuntimeOutcome::record_additional_contexts。它是“钩子补充内容进入对话”的统一入口。

调用图:调用 1 个内部函数(additional_context_messages);被 6 处调用(record_additional_contexts, record_pending_input, run_pre_tool_use_hooks, run_hooks_and_record_inputs, on_task_finished, dispatch_any_with_terminal_outcome)。

additional_context_messages609–615 ↗
fn additional_context_messages(additional_contexts: Vec<String>) -> Vec<ResponseItem>

作用:把普通字符串形式的额外上下文,转换成协议里的 ResponseItem 消息。简单说,就是把便签纸变成系统能归档的正式消息。

数据流:进去的是字符串列表 → 每个字符串先包成 HookAdditionalContext,再转成 ContextualUserFragment,最后变成 ResponseItem → 出来的是一组顺序不变的对话消息。

调用关系:它主要被 record_additional_contexts 使用,也被测试 additional_context_messages_stay_separate_and_ordered 检查是否保持顺序和分隔。

调用图:被 2 处调用(record_additional_contexts, additional_context_messages_stay_separate_and_ordered)。

emit_hook_started_events617–632 ↗
async fn emit_hook_started_events(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    preview_runs: Vec<HookRunSummary>,
)

作用:把即将运行的钩子通知给外部。界面或客户端可以据此显示“某个钩子开始跑了”。

数据流:进去的是会话、回合和一组钩子运行摘要 → 它逐个包装成 HookStarted 事件,并通过会话发送出去 → 结果是外部观察者能看到每个钩子的开始状态。

调用关系:它被几乎所有新版钩子流程调用,包括工具前后、权限请求、压缩前后、停止钩子和 run_context_injecting_hook。

调用图:被 7 处调用(run_context_injecting_hook, run_permission_request_hooks, run_post_compact_hooks, run_post_tool_use_hooks, run_pre_compact_hooks, run_pre_tool_use_hooks, run_turn_stop_hooks);外部调用 1 个(HookStarted)。

emit_hook_completed_events634–645 ↗
async fn emit_hook_completed_events(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    completed_events: Vec<HookCompletedEvent>,
)

作用:把钩子完成的消息发出去,同时补上指标和分析记录。它是钩子运行结束后的统一收银台。

数据流:进去的是会话、回合和一组完成事件 → 对每个完成事件,它先记录运行次数和耗时指标,再发送分析事件,最后把 HookCompleted 事件发给客户端 → 结果是用户界面、监控系统和分析系统都知道这个钩子结束了。

调用关系:它被所有新版钩子运行流程调用。内部会把指标工作交给 emit_hook_completed_metrics,把分析上报交给 track_hook_completed_analytics。

调用图:调用 2 个内部函数(emit_hook_completed_metrics, track_hook_completed_analytics);被 7 处调用(run_context_injecting_hook, run_permission_request_hooks, run_post_compact_hooks, run_post_tool_use_hooks, run_pre_compact_hooks, run_pre_tool_use_hooks, run_turn_stop_hooks);外部调用 1 个(HookCompleted)。

emit_hook_completed_metrics647–661 ↗
fn emit_hook_completed_metrics(turn_context: &TurnContext, completed: &HookCompletedEvent)

作用:记录一个钩子完成后的监控指标,比如跑了几次、用了多久。指标就是给运维和开发看的统计数字。

数据流:进去的是回合上下文和一个钩子完成事件 → 它先用 hook_run_metric_tags 算出标签,比如钩子名字、来源、状态;然后计数加一;如果有耗时,就把毫秒数转成时间长度并记录 → 结果是遥测系统里多了一条运行统计。

调用关系:它只由 emit_hook_completed_events 调用。这样所有钩子完成时都会走同一套指标格式。

调用图:调用 1 个内部函数(hook_run_metric_tags);被 1 处调用(emit_hook_completed_events);外部调用 2 个(from_millis, try_from)。

track_hook_completed_analytics663–673 ↗
fn track_hook_completed_analytics(
    sess: &Arc<Session>,
    turn_context: &Arc<TurnContext>,
    completed: &HookCompletedEvent,
)

作用:把钩子完成这件事上报到分析系统,用于了解不同来源和状态的钩子运行情况。

数据流:进去的是会话、回合和完成事件 → 它调用 hook_run_analytics_payload 生成分析上下文和事实数据 → 然后通过会话里的 analytics_events_client 发送出去。

调用关系:它由 emit_hook_completed_events 调用,和 emit_hook_completed_metrics 并排工作:一个偏产品分析,一个偏运行监控。

调用图:调用 1 个内部函数(hook_run_analytics_payload);被 1 处调用(emit_hook_completed_events)。

hook_run_analytics_payload675–695 ↗
fn hook_run_analytics_payload(
    thread_id: String,
    turn_context: &TurnContext,
    completed: &HookCompletedEvent,
) -> (codex_analytics::TrackEventsContext, HookRunFact)

作用:把一个钩子完成事件整理成分析系统需要的两块数据:这次运行属于哪个线程和回合,以及钩子本身是什么状态。

数据流:进去的是线程编号、回合上下文和完成事件 → 它优先使用完成事件里带的 turn_id,如果没有就退回当前回合编号;再提取模型名、事件名、钩子来源和状态 → 出来的是分析上下文和 HookRunFact。

调用关系:它被 track_hook_completed_analytics 调用,也被测试检查两种 turn_id 情况是否正确。

调用图:被 3 处调用(hook_run_analytics_payload_falls_back_to_turn_context_id, hook_run_analytics_payload_uses_completed_turn_id, track_hook_completed_analytics);外部调用 1 个(build_track_events_context)。

hook_run_metric_tags697–736 ↗
fn hook_run_metric_tags(run: &HookRunSummary) -> [(&'static str, &'static str); 3]

作用:把钩子运行摘要里的枚举值,翻译成监控系统能稳定识别的短文字标签。

数据流:进去的是 HookRunSummary,里面有钩子事件名、来源和状态 → 它把这些内部枚举分别映射成固定字符串,比如 Stop、project、blocked → 出来的是三组标签,供指标记录使用。

调用关系:它由 emit_hook_completed_metrics 调用。相关测试会确认新旧钩子来源都能映射成预期的标签。

调用图:被 1 处调用(emit_hook_completed_metrics)。

hook_permission_mode738–747 ↗
fn hook_permission_mode(turn_context: &TurnContext) -> String

作用:把当前回合的审批策略,翻译成钩子协议里使用的权限模式文字。这样钩子不需要理解内部复杂的审批枚举。

数据流:进去的是 TurnContext → 它读取 approval_policy:如果是永不询问,就输出 bypassPermissions;其他需要默认权限判断的情况输出 default → 出来的是一个字符串。

调用关系:它被会话开始、用户输入、工具前后、权限请求和停止钩子等流程调用,用来保证每类钩子看到一致的权限字段。

调用图:被 6 处调用(inspect_pending_input, run_pending_session_start_hooks, run_permission_request_hooks, run_post_tool_use_hooks, run_pre_tool_use_hooks, run_turn_stop_hooks)。

thread_spawn_subagent_hook_context749–759 ↗
fn thread_spawn_subagent_hook_context(
    sess: &Arc<Session>,
    turn_context: &TurnContext,
) -> Option<SubagentHookContext>

作用:如果当前回合属于线程派生的子代理,就生成子代理身份信息;否则返回空。子代理可以理解为主任务临时派出去的小助手。

数据流:进去的是会话和回合上下文 → 它查看 session_source 是否是 ThreadSpawn 类型的 SubAgent;如果是,就调用 subagent_hook_context 生成 agent_id 和 agent_type;如果不是,返回 None → 出来的是可选的子代理上下文。

调用关系:它被工具前后、权限请求、压缩前后和用户输入钩子使用。这样这些钩子能知道自己是不是在子代理里运行。

调用图:调用 1 个内部函数(subagent_hook_context);被 6 处调用(inspect_pending_input, run_permission_request_hooks, run_post_compact_hooks, run_post_tool_use_hooks, run_pre_compact_hooks, run_pre_tool_use_hooks)。

subagent_hook_context761–768 ↗
fn subagent_hook_context(sess: &Arc<Session>, agent_role: &Option<String>) -> SubagentHookContext

作用:根据当前会话和子代理角色,生成钩子协议需要的子代理身份。它给子代理贴上编号和类型标签。

数据流:进去的是会话和可选的代理角色名 → 它用会话线程编号作为 agent_id;如果角色名存在就用它,否则使用默认角色名 → 出来的是 SubagentHookContext。

调用关系:它被 run_pending_session_start_hooks、run_turn_stop_hooks 和 thread_spawn_subagent_hook_context 调用,是生成子代理钩子身份的底层小工具。

调用图:被 3 处调用(run_pending_session_start_hooks, run_turn_stop_hooks, thread_spawn_subagent_hook_context)。

compaction_trigger_label770–775 ↗
fn compaction_trigger_label(value: CompactionTrigger) -> &'static str

作用:把上下文压缩的触发原因翻译成钩子能读懂的固定文字。手动触发就是 manual,自动触发就是 auto。

数据流:进去的是 CompactionTrigger 枚举 → 它根据是 Manual 还是 Auto 做简单匹配 → 出来的是一个静态字符串标签。

调用关系:它被 run_pre_compact_hooks 和 run_post_compact_hooks 使用,保证压缩前后钩子看到同样格式的触发原因。

调用图:被 2 处调用(run_post_compact_hooks, run_pre_compact_hooks)。

tests::additional_context_messages_stay_separate_and_ordered798–829 ↗
fn additional_context_messages_stay_separate_and_ordered()

作用:测试额外上下文转换成消息后,不能被合并,也不能乱序。因为钩子返回的多段说明,顺序可能有意义。

数据流:进去的是两段测试字符串 → 它调用 additional_context_messages 转换,再检查结果数量、角色和文字内容 → 如果顺序或内容不对,测试失败。

调用关系:它直接验证 additional_context_messages 的行为,保护 record_additional_contexts 依赖的基础转换逻辑。

调用图:调用 1 个内部函数(additional_context_messages);外部调用 2 个(assert_eq!, vec!)。

tests::hook_run_analytics_payload_uses_completed_turn_id832–848 ↗
async fn hook_run_analytics_payload_uses_completed_turn_id()

作用:测试分析数据在完成事件自带 turn_id 时,会优先使用这个 turn_id。

数据流:进去的是一个测试会话上下文和带有 turn-from-hook 的完成事件 → 它调用 hook_run_analytics_payload → 检查输出里的线程、回合、模型、事件名、来源和状态都符合预期。

调用关系:它验证 hook_run_analytics_payload 的优先级规则,确保 track_hook_completed_analytics 上报时不会错用回合编号。

调用图:调用 2 个内部函数(hook_run_analytics_payload, make_session_and_context);外部调用 2 个(assert_eq!, sample_hook_run)。

tests::hook_run_analytics_payload_falls_back_to_turn_context_id851–864 ↗
async fn hook_run_analytics_payload_falls_back_to_turn_context_id()

作用:测试当完成事件没有 turn_id 时,分析数据会退回使用当前回合上下文里的编号。

数据流:进去的是测试会话上下文和一个 turn_id 为空的完成事件 → 它调用 hook_run_analytics_payload → 检查输出 turn_id 等于 turn_context.sub_id,并检查来源和状态保留正确。

调用关系:它补上 hook_run_analytics_payload 的另一条分支,保证老数据或不完整事件也能正常上报。

调用图:调用 2 个内部函数(hook_run_analytics_payload, make_session_and_context);外部调用 2 个(assert_eq!, sample_hook_run)。

tests::hook_run_metric_tags_match_analytics_shape867–890 ↗
fn hook_run_metric_tags_match_analytics_shape()

作用:测试监控指标标签的形状和分析数据保持一致,特别是钩子名、来源和状态的文字。

数据流:进去的是一个模拟的钩子运行摘要 → 它调用 hook_run_metric_tags 并对比预期标签;还额外检查 cloud_requirements 这个来源的输出 → 如果标签文字变了,测试会失败。

调用关系:它保护 hook_run_metric_tags,避免监控标签被无意改坏,影响仪表盘或查询。

调用图:外部调用 2 个(assert_eq!, sample_hook_run)。

tests::hook_run_metric_tags_include_expanded_hook_sources893–904 ↗
fn hook_run_metric_tags_include_expanded_hook_sources()

作用:测试较新的或扩展过的钩子来源也能被正确写成指标标签。

数据流:进去的是来源为 LegacyManagedConfigMdm 的模拟运行摘要 → 它调用 hook_run_metric_tags → 检查输出 source 是 legacy_managed_config_mdm,状态是 completed。

调用关系:它同样围绕 hook_run_metric_tags,确保支持更多 HookSource 时不会漏映射。

调用图:外部调用 2 个(assert_eq!, sample_hook_run)。

tests::sample_hook_run906–923 ↗
fn sample_hook_run(status: HookRunStatus, source: HookSource) -> HookRunSummary

作用:生成一份测试用的钩子运行摘要,避免每个测试都手写一大段重复数据。

数据流:进去的是想要测试的状态和来源 → 它填入固定的编号、事件名、处理器类型、时间、路径等字段 → 出来的是 HookRunSummary,供多个测试复用。

调用关系:它被多个测试函数调用,是测试里的小工厂函数,不参与正式运行流程。

调用图:外部调用 2 个(new, test_path_buf)。

hooks/src/legacy_notify.rs源码 ↗
io_transportagent turn complete hook

这个文件解决的是“新系统怎么继续喂给老通知脚本同样数据”的问题。可以把它想成一个翻译员:系统内部有一份 HookPayload,也就是钩子收到的事件包;旧脚本只认最后一个命令行参数里的 JSON 字符串。这里先定义 UserNotification,规定旧脚本看到的 JSON 长什么样,比如线程编号、这一轮编号、当前目录、用户输入和最后一条助手回复。legacy_notify_json 负责把内部事件翻译成这份旧格式。notify_hook 则把用户给的命令保存起来,等钩子触发时,组装命令、把 JSON 塞到最后一个参数,然后静默启动这个外部程序。静默的意思是标准输入、输出、错误都接到空设备,避免通知脚本干扰主程序。下面的测试专门保证 JSON 字段名和历史格式一致,防止看似小的改名把老脚本弄坏。

函数细节5
legacy_notify_json28–41 ↗
fn legacy_notify_json(payload: &HookPayload) -> Result<String, serde_json::Error>

作用:把系统内部的钩子事件包转换成旧版通知脚本能读懂的 JSON 字符串。有人会用它,是因为外部脚本通常只会拿到文本参数,而不是 Rust 里的复杂对象。

数据流:进去的是 HookPayload,里面带着当前目录、客户端名,以及“智能体一轮结束后”的事件信息。它取出线程编号、轮次编号、输入消息和最后一条助手消息,放进 UserNotification::AgentTurnComplete 这种固定模板里,再用 serde_json 转成字符串。出来的是一个 JSON 字符串;如果转换失败,就返回错误,不改动原始 payload。

调用关系:它是内部数据到旧版通知格式之间的翻译步骤。测试函数 tests::legacy_notify_json_matches_historical_wire_shape 会直接调用它,确认它产出的 JSON 还和过去的外部接口一模一样;在实际通知流程里,它也被 notify_hook 创建的钩子用来给外部命令准备最后一个参数。

调用图:被 1 处调用(legacy_notify_json_matches_historical_wire_shape);外部调用 1 个(to_string)。

notify_hook43–70 ↗
fn notify_hook(argv: Vec<String>) -> Hook

作用:根据一串命令行参数创建一个名叫 legacy_notify 的钩子。这个钩子触发时会启动外部通知程序,并把本轮结束信息作为最后一个参数交给它。

数据流:进去的是 argv,也就是用户配置的命令和参数列表。函数先把它放进 Arc(一种可共享的引用,让异步钩子以后还能安全拿到这份命令),然后返回一个 Hook。等这个 Hook 真正运行时,它会根据 argv 造出系统命令;如果命令为空,就直接算成功;如果能把 payload 转成旧版 JSON,就把 JSON 追加到命令末尾;接着关闭输入输出通道并启动进程。启动成功返回 HookResult::Success,启动失败则返回 HookResult::FailedContinue,意思是记录失败但主流程继续走。

调用关系:它是把“旧版通知能力”接进钩子系统的入口。外部配置或上层钩子加载代码会调用它来得到 Hook;之后 Hook 系统在合适时机执行这个 Hook。它内部会准备共享命令数据,并在触发时依赖 legacy_notify_json 把事件包翻译成旧脚本需要的文本。

调用图:外部调用 1 个(new)。

tests::expected_notification_json85–96 ↗
fn expected_notification_json() -> Value

作用:准备一份测试用的“标准答案”JSON。它说明旧版通知消息应该长什么样,测试时拿实际结果来和它比较。

数据流:进去没有外部参数。它用测试路径构造当前目录,再用 json! 宏拼出一份包含 type、thread-id、turn-id、cwd、client、input-messages 和 last-assistant-message 的 JSON 值。出来的是 serde_json::Value,供测试断言使用。

调用关系:它是两个测试共同使用的参照物。tests::test_user_notification 和 tests::legacy_notify_json_matches_historical_wire_shape 都会把实际序列化结果转成 JSON 值,再和这个标准答案比较,确保字段名和内容没有跑偏。

调用图:外部调用 2 个(test_path_buf, json!)。

tests::test_user_notification99–116 ↗
fn test_user_notification() -> Result<()>

作用:验证 UserNotification 这个旧版通知结构本身序列化后,字段名和内容都符合历史约定。这样可以防止改结构体或 serde 设置时不小心破坏外部脚本。

数据流:进去没有业务输入,测试里手动造出一个 AgentTurnComplete 通知对象。它把对象转成 JSON 字符串,再解析回通用 JSON 值,最后和 tests::expected_notification_json 给出的标准答案比较。出来的是测试结果:完全一致就通过;不一致就失败并指出差异。

调用关系:它直接测试最底层的通知 JSON 形状,不经过 HookPayload。它会调用 tests::expected_notification_json 拿标准答案,并用 assert_eq! 做比较;这相当于给 UserNotification 的对外格式加了一把保险锁。

调用图:外部调用 5 个(assert_eq!, test_path_buf, from_str, to_string, vec!)。

tests::legacy_notify_json_matches_historical_wire_shape119–145 ↗
fn legacy_notify_json_matches_historical_wire_shape() -> Result<()>

作用:验证 legacy_notify_json 从真实 HookPayload 翻译出来的 JSON,也和旧版外部接口完全一致。它测试的是完整转换流程,而不只是结构体单独序列化。

数据流:进去没有外部参数,测试里构造一个 HookPayload:包含会话编号、当前目录、客户端名、触发时间,以及 AfterAgent 事件。它把这个 payload 交给 legacy_notify_json,得到 JSON 字符串,再解析成 JSON 值,最后和标准答案比较。出来的是测试是否通过;如果翻译结果字段名、字段值或缺失规则变了,测试就会失败。

调用关系:它站在调用者角度检查 legacy_notify_json,是这个文件里最贴近真实使用场景的回归测试。它会创建测试用线程编号和路径,调用 legacy_notify_json,再用 tests::expected_notification_json 的标准答案确认旧脚本收到的数据格式没有变化。

调用图:调用 3 个内部函数(legacy_notify_json, from_string, new);外部调用 5 个(assert_eq!, now, test_path_buf, from_str, vec!)。

引擎基础

这些文件发现已配置的钩子,组装引擎,执行命令,解析输出,并管理超大输出的处理。

hooks/src/engine/mod.rs源码 ↗
orchestrationstartup and request handling

hook 可以理解成“某个时刻自动触发的小脚本”,比如会话开始、工具运行前、用户提交提示词时。这个文件定义了 ClaudeHooksEngine,也就是这套 hook 机器的入口控制器。启动时,它会按开关决定是否启用 hook;启用后会加载 schema(用来检查 hook 配置格式的规则),再通过 discovery 找出配置文件和插件里声明的 handler(真正要运行的命令)。运行中,外部代码不直接碰各个事件模块,而是调用这里的 preview_xxx 或 run_xxx。preview 只告诉你“将会跑哪些 hook”,run 才真的执行。它还统一处理一件容易被忽略的事:hook 输出太长时,通过 HookOutputSpiller 把内容“溢出”到更合适的地方,避免结果塞爆上下文。ConfiguredHandler 则像一张 hook 工单,记着事件名、匹配条件、命令、超时、来源和信任状态等信息。

函数细节23
ConfiguredHandler::run_id55–62 ↗
fn run_id(&self) -> String

作用:给一个已配置的 hook 生成一个稳定的运行编号,方便日志、摘要或界面区分“这次跑的是哪一个 hook”。

数据流:进去的是这个 handler 自己保存的事件名、显示顺序和来源路径 → 它把事件名转成好读的短标签,再和顺序、文件路径拼成一个字符串 → 出来的是类似身份证的 run_id,不改动任何数据。

调用关系:运行摘要需要展示某个 hook 的身份时会用到它,比如 completed_summary 和 running_summary 会拿这个编号标记已完成或正在运行的 hook。

调用图:被 2 处调用(completed_summary, running_summary);外部调用 1 个(format!)。

ConfiguredHandler::event_name_label64–77 ↗
fn event_name_label(&self) -> &'static str

作用:把内部使用的事件枚举名,翻译成人容易看懂、适合显示和拼接的短文本。枚举就是一组固定选项,比如“工具运行前”“会话开始”。

数据流:进去的是 handler 里的 event_name → 它根据具体事件选择对应的小写短横线写法 → 出来的是固定字符串,例如 PreToolUse 变成 pre-tool-use。

调用关系:它主要服务于 ConfiguredHandler::run_id,让生成的运行编号既稳定又容易读。

ClaudeHooksEngine::new108–138 ↗
fn new(
        enabled: bool,
        bypass_hook_trust: bool,
        config_layer_stack: Option<&ConfigLayerStack>,
        plugin_hook_sources: Vec<PluginHookSource>,
        plugin_hook_load_warn

作用:创建 hook 引擎,并在启动时把可用的 hook 全部找出来。它也会尊重总开关:如果 hook 被关闭,就创建一个空引擎,什么 hook 都不会跑。

数据流:进去的是是否启用、是否跳过信任检查、配置层、插件 hook 来源、插件加载警告和要用的 shell 命令外壳 → 如果未启用,就保存空 handler 和空警告;如果启用,就加载 hook schema,再调用发现流程收集 handler 和警告 → 出来的是一个可供后续预览和运行 hook 的 ClaudeHooksEngine。

调用关系:这是整个 hook 系统启动时的入口。许多测试都会通过它验证不同配置下能发现哪些 hook、哪些警告会留下来;它把具体查找工作交给 discovery::discover_handlers,把输出过长处理器初始化为 HookOutputSpiller。

调用图:调用 3 个内部函数(discover_handlers, generated_hook_schemas, new);被 18 处调用(allow_managed_hooks_only_false_keeps_unmanaged_hooks, allow_managed_hooks_only_in_config_toml_does_not_enable_policy, allow_managed_hooks_only_keeps_managed_requirement_and_config_layer_hooks, allow_managed_hooks_only_skips_unmanaged_json_and_toml_hooks, allow_managed_hooks_only_skips_unmanaged_plugin_hooks, discovers_hooks_from_json_and_toml_in_the_same_layer, malformed_hooks_json_is_reported_as_startup_warning, plugin_hook_load_warnings_are_startup_warnings, plugin_hook_sources_expand_plugin_placeholders, plugin_hook_sources_run_with_plugin_env_and_plugin_source (+8 more));外部调用 1 个(new)。

ClaudeHooksEngine::warnings140–142 ↗
fn warnings(&self) -> &[String]

作用:把启动时收集到的 hook 警告交出去,比如配置写错、插件 hook 加载失败等。这样外层程序可以提醒用户,而不是默默忽略问题。

数据流:进去的是引擎自身保存的 warnings 列表 → 它不复制也不修改,只返回一个只读引用 → 出来的是调用者能查看的警告文本列表。

调用关系:启动警告展示流程会调用它,例如 startup_warnings 会读取这些信息并拿去报告给用户。

调用图:被 1 处调用(startup_warnings)。

ClaudeHooksEngine::preview_session_start144–149 ↗
fn preview_session_start(
        &self,
        request: &SessionStartRequest,
    ) -> Vec<HookRunSummary>

作用:在“会话开始”事件真的执行前,预先列出会跑哪些 hook。适合给用户看计划,或让系统做确认。

数据流:进去的是会话开始请求和引擎里已经发现的 handler 列表 → 它把这些交给 session_start 事件模块的 preview → 出来的是一组 HookRunSummary,也就是 hook 运行摘要。

调用关系:外部的 preview_session_start 流程会调用它;它本身不判断细节,而是把筛选和摘要生成交给 crate::events::session_start::preview。

调用图:调用 1 个内部函数(preview);被 1 处调用(preview_session_start)。

ClaudeHooksEngine::preview_pre_tool_use151–153 ↗
fn preview_pre_tool_use(&self, request: &PreToolUseRequest) -> Vec<HookRunSummary>

作用:在工具真正被使用前,预先说明哪些“工具使用前”hook 会被触发。这样可以提前知道可能会有拦截、提示或检查。

数据流:进去的是工具使用前请求和 handler 列表 → 它调用 pre_tool_use 事件模块的 preview 做匹配 → 出来的是将要运行的 hook 摘要列表。

调用关系:外部的 preview_pre_tool_use 流程会用它;它只是总控入口,具体如何匹配某个工具由 crate::events::pre_tool_use::preview 处理。

调用图:调用 1 个内部函数(preview);被 1 处调用(preview_pre_tool_use)。

ClaudeHooksEngine::preview_permission_request155–160 ↗
fn preview_permission_request(
        &self,
        request: &PermissionRequestRequest,
    ) -> Vec<HookRunSummary>

作用:在权限请求处理前,预览哪些权限相关 hook 会参与判断。它帮助系统提前展示“谁可能影响这次允许或拒绝”。

数据流:进去的是权限请求和已配置 handler → 它交给 permission_request 事件模块生成预览 → 出来的是 hook 运行摘要列表。

调用关系:外部的 preview_permission_request 会调用它;它把具体预览逻辑交给 crate::events::permission_request::preview。

调用图:调用 1 个内部函数(preview);被 1 处调用(preview_permission_request)。

ClaudeHooksEngine::preview_post_tool_use162–167 ↗
fn preview_post_tool_use(
        &self,
        request: &PostToolUseRequest,
    ) -> Vec<HookRunSummary>

作用:在工具运行结束后但 hook 尚未执行前,预览哪些“工具使用后”hook 会运行。常用于展示后处理或反馈步骤。

数据流:进去的是工具使用后的请求和 handler 列表 → 它调用 post_tool_use 事件模块做预览 → 出来的是将要执行的 hook 摘要。

调用关系:外部的 preview_post_tool_use 流程会进到这里;它把事件专属的判断交给 crate::events::post_tool_use::preview。

调用图:调用 1 个内部函数(preview);被 1 处调用(preview_post_tool_use)。

ClaudeHooksEngine::run_session_start169–181 ↗
async fn run_session_start(
        &self,
        request: SessionStartRequest,
        turn_id: Option<String>,
    ) -> SessionStartOutcome

作用:真正运行“会话开始”hook,并整理它们额外提供给会话的上下文内容。上下文太长时,它会自动做溢出处理,避免把主流程撑得过大。

数据流:进去的是会话开始请求和可选 turn_id → 它先记下 session_id,再把 handler、shell 和请求交给 session_start 的 run 执行 → 得到 outcome 后,把 additional_contexts 交给 maybe_spill_texts 检查是否需要外置保存 → 出来的是更新后的 SessionStartOutcome。

调用关系:外部的 run_session_start 会调用它;它负责串起事件模块的实际执行和统一的输出溢出处理。

调用图:调用 2 个内部函数(maybe_spill_texts, run);被 1 处调用(run_session_start)。

ClaudeHooksEngine::run_pre_tool_use183–191 ↗
async fn run_pre_tool_use(&self, request: PreToolUseRequest) -> PreToolUseOutcome

作用:真正运行“工具使用前”hook,用来在工具执行前做检查、补充上下文或决定后续行为。

数据流:进去的是工具使用前请求 → 它保存 session_id,把 handler、shell 和请求交给 pre_tool_use 的 run → 再把返回的 additional_contexts 经过 maybe_spill_texts 处理 → 出来的是最终的 PreToolUseOutcome。

调用关系:外部的 run_pre_tool_use 流程会到这里;它把真正执行交给 crate::events::pre_tool_use::run,把长文本处理交给自己的 maybe_spill_texts。

调用图:调用 2 个内部函数(maybe_spill_texts, run);被 1 处调用(run_pre_tool_use)。

ClaudeHooksEngine::run_permission_request193–198 ↗
async fn run_permission_request(
        &self,
        request: PermissionRequestRequest,
    ) -> PermissionRequestOutcome

作用:真正运行权限请求 hook,让外部命令有机会参与“这件事允不允许做”的判断。

数据流:进去的是权限请求 → 它把 handler、shell 和请求交给 permission_request 的 run → 出来的是 PermissionRequestOutcome,里面包含权限 hook 的结果。

调用关系:外部的 run_permission_request 会调用它;这个函数不做额外溢出处理,因为权限结果不像上下文文本那样需要被塞回大量内容。

调用图:调用 1 个内部函数(run);被 1 处调用(run_permission_request)。

ClaudeHooksEngine::run_post_tool_use200–214 ↗
async fn run_post_tool_use(
        &self,
        request: PostToolUseRequest,
    ) -> PostToolUseOutcome

作用:真正运行“工具使用后”hook,用来处理工具结束后的反馈、补充信息或提示。它会特别照顾可能很长的反馈文本。

数据流:进去的是工具使用后的请求 → 它记下 session_id,调用 post_tool_use 的 run 执行 hook → 然后分别处理 additional_contexts 和 feedback_message:多个上下文走 maybe_spill_texts,单条反馈走 maybe_spill_text → 出来的是整理后的 PostToolUseOutcome。

调用关系:外部的 run_post_tool_use 会调用它;它连接了 post_tool_use 事件模块和 HookOutputSpiller,确保执行结果既能返回,又不会因为太长影响主上下文。

调用图:调用 3 个内部函数(maybe_spill_text, maybe_spill_texts, run);被 1 处调用(run_post_tool_use)。

ClaudeHooksEngine::preview_pre_compact216–218 ↗
fn preview_pre_compact(&self, request: &PreCompactRequest) -> Vec<HookRunSummary>

作用:在压缩上下文前,预览哪些 pre-compact hook 会运行。compact 可以理解成“把长对话整理变短”,pre-compact 就是整理前的检查点。

数据流:进去的是压缩前请求和 handler 列表 → 它调用 compact 模块的 preview_pre → 出来的是将要运行的 hook 摘要。

调用关系:外部的 preview_pre_compact 流程会调用它;具体哪些 hook 匹配压缩前事件,由 crate::events::compact::preview_pre 决定。

调用图:调用 1 个内部函数(preview_pre);被 1 处调用(preview_pre_compact)。

ClaudeHooksEngine::run_pre_compact220–222 ↗
async fn run_pre_compact(&self, request: PreCompactRequest) -> PreCompactOutcome

作用:真正运行压缩上下文前的 hook,让脚本有机会在整理对话前做决定或补充处理。

数据流:进去的是 PreCompactRequest → 它把 handler、shell 和请求交给 compact 模块的 run_pre → 出来的是 PreCompactOutcome。

调用关系:外部的 run_pre_compact 会调用它;它作为总控入口,把事件执行交给 crate::events::compact::run_pre。

调用图:调用 1 个内部函数(run_pre);被 1 处调用(run_pre_compact)。

ClaudeHooksEngine::preview_post_compact224–226 ↗
fn preview_post_compact(&self, request: &PostCompactRequest) -> Vec<HookRunSummary>

作用:在压缩上下文完成后,预览哪些 post-compact hook 会运行。它让系统提前知道整理完成后还会触发哪些脚本。

数据流:进去的是 PostCompactRequest 和 handler 列表 → 它交给 compact 模块的 preview_post → 出来的是 hook 运行摘要列表。

调用关系:外部的 preview_post_compact 会调用它;事件专属的筛选逻辑放在 crate::events::compact::preview_post。

调用图:调用 1 个内部函数(preview_post);被 1 处调用(preview_post_compact)。

ClaudeHooksEngine::run_post_compact228–233 ↗
async fn run_post_compact(
        &self,
        request: PostCompactRequest,
    ) -> StatelessHookOutcome

作用:真正运行压缩上下文后的 hook,用于在对话整理完成后做收尾动作。这里返回的是无状态结果,意思是结果不依赖额外会话状态累积。

数据流:进去的是 PostCompactRequest → 它把 handler、shell 和请求交给 compact 模块的 run_post → 出来的是 StatelessHookOutcome。

调用关系:外部的 run_post_compact 会调用它;它只负责分发,具体命令执行由 crate::events::compact::run_post 完成。

调用图:调用 1 个内部函数(run_post);被 1 处调用(run_post_compact)。

ClaudeHooksEngine::preview_user_prompt_submit235–240 ↗
fn preview_user_prompt_submit(
        &self,
        request: &UserPromptSubmitRequest,
    ) -> Vec<HookRunSummary>

作用:在用户提交提示词时,预览哪些 hook 会被触发。它能提前显示用户这句话进入系统前会经过哪些自动脚本。

数据流:进去的是用户提示词提交请求和 handler 列表 → 它调用 user_prompt_submit 模块的 preview → 出来的是将要运行的 hook 摘要。

调用关系:外部的 preview_user_prompt_submit 会调用它;它把匹配和摘要生成交给 crate::events::user_prompt_submit::preview。

调用图:调用 1 个内部函数(preview);被 1 处调用(preview_user_prompt_submit)。

ClaudeHooksEngine::run_user_prompt_submit242–253 ↗
async fn run_user_prompt_submit(
        &self,
        request: UserPromptSubmitRequest,
    ) -> UserPromptSubmitOutcome

作用:真正运行“用户提交提示词”hook,让脚本有机会在用户输入进入后续流程前添加上下文或产生结果。

数据流:进去的是 UserPromptSubmitRequest → 它记下 session_id,调用 user_prompt_submit 的 run 执行 hook → 再把 outcome.additional_contexts 交给 maybe_spill_texts 处理长文本 → 出来的是最终 UserPromptSubmitOutcome。

调用关系:外部的 run_user_prompt_submit 会调用它;它把事件执行交给 crate::events::user_prompt_submit::run,把长输出收纳交给 HookOutputSpiller。

调用图:调用 2 个内部函数(maybe_spill_texts, run);被 1 处调用(run_user_prompt_submit)。

ClaudeHooksEngine::preview_stop255–257 ↗
fn preview_stop(&self, request: &StopRequest) -> Vec<HookRunSummary>

作用:在停止事件发生时,预览哪些 stop hook 会运行。停止事件通常是一次会话或一段流程结束前后的收尾点。

数据流:进去的是 StopRequest 和 handler 列表 → 它调用 stop 模块的 preview → 出来的是 hook 运行摘要列表。

调用关系:外部的 preview_stop 会调用它;具体哪些停止 hook 匹配,由 crate::events::stop::preview 处理。

调用图:调用 1 个内部函数(preview);被 1 处调用(preview_stop)。

ClaudeHooksEngine::run_stop259–266 ↗
async fn run_stop(&self, request: StopRequest) -> StopOutcome

作用:真正运行 stop hook,并处理它们返回的 continuation fragments。continuation fragments 可以理解成“想让后续继续使用的小段提示内容”。

数据流:进去的是 StopRequest → 它记下 session_id,调用 stop 模块的 run 执行 hook → 然后把 continuation_fragments 交给 maybe_spill_prompt_fragments,必要时把过长内容外置 → 出来的是整理后的 StopOutcome。

调用关系:外部的 run_stop 会调用它;它把停止事件的实际执行交给 crate::events::stop::run,再用统一的溢出机制保护后续上下文。

调用图:调用 2 个内部函数(maybe_spill_prompt_fragments, run);被 1 处调用(run_stop)。

ClaudeHooksEngine::maybe_spill_texts268–272 ↗
async fn maybe_spill_texts(&self, session_id: ThreadId, texts: Vec<String>) -> Vec<String>

作用:检查一组文本是否太长,太长就通过输出溢出器换成更安全的表示。可以把它想成把大包裹寄存到柜台,只在手里拿一张取件条。

数据流:进去的是 session_id 和一组文本 → 它调用 output_spiller.maybe_spill_texts 判断并处理每段文本 → 出来的是处理后的文本列表,可能仍是原文,也可能是指向外置内容的替代文本。

调用关系:run_session_start、run_pre_tool_use、run_post_tool_use 和 run_user_prompt_submit 都会在拿到 hook 的额外上下文后调用它,避免长输出直接塞进主流程。

调用图:调用 1 个内部函数(maybe_spill_texts);被 4 处调用(run_post_tool_use, run_pre_tool_use, run_session_start, run_user_prompt_submit)。

ClaudeHooksEngine::maybe_spill_text274–279 ↗
async fn maybe_spill_text(&self, session_id: ThreadId, text: Option<String>) -> Option<String>

作用:检查一条可有可无的文本是否太长;没有文本就什么也不做,有文本才交给溢出器处理。

数据流:进去的是 session_id 和 Option<String>,Option 的意思是“可能有,也可能没有” → 如果是 None,直接返回 None;如果是 Some 文本,就调用 output_spiller.maybe_spill_text → 出来的是同样可有可无、但已处理过的文本。

调用关系:run_post_tool_use 用它处理 feedback_message,因为工具使用后的反馈是一条可选消息,不一定每次都有。

调用图:调用 1 个内部函数(maybe_spill_text);被 1 处调用(run_post_tool_use)。

ClaudeHooksEngine::maybe_spill_prompt_fragments281–289 ↗
async fn maybe_spill_prompt_fragments(
        &self,
        session_id: ThreadId,
        fragments: Vec<codex_protocol::items::HookPromptFragment>,
    ) -> Vec<codex_protocol::items::HookPromptFra

作用:检查一组提示片段是否太大,必要时把内容外置,避免停止事件返回的续写提示占用过多空间。

数据流:进去的是 session_id 和一组 HookPromptFragment → 它交给 output_spiller.maybe_spill_prompt_fragments 逐个处理 → 出来的是处理后的提示片段列表,可能包含对外置内容的引用。

调用关系:run_stop 在 stop hook 执行完后调用它,专门处理 continuation_fragments 这类提示片段形式的输出。

调用图:调用 1 个内部函数(maybe_spill_prompt_fragments);被 1 处调用(run_stop)。

hooks/src/engine/discovery.rs源码 ↗
orchestrationconfig load / startup

这份文件像一个安检员加排班员。它会从多种地方读取 hook 配置:系统配置、用户配置、项目里的 hooks.json 或 config.toml、企业/MDM 管理配置,以及插件带来的 hook。读到以后,它不会立刻全放行,而是先判断来源是不是受管理的、用户有没有禁用、命令有没有被信任、匹配规则是否合法。最后它产出两份东西:一份是真正会执行的 handler,也就是可运行的 hook;另一份是展示给用户看的 hook 列表,里面包括来源、状态、是否可信等信息。这样做很重要,因为 hook 本质上会跑命令,如果不区分来源和信任状态,就可能把用户没确认过或被改过的脚本直接执行,带来安全风险。

函数细节36
HookDiscoveryPolicy::allows58–60 ↗
fn allows(self, source: &HookHandlerSource<'_>) -> bool

作用:判断某个 hook 来源是否符合当前发现策略。最主要的规则是:如果系统要求“只允许受管理的 hook”,那普通用户或插件来源就不能通过。

数据流:输入是一条 hook 来源信息和当前策略 → 它查看策略里的 allow_managed_hooks_only,再看来源是不是 managed → 输出 true 或 false,表示后面要不要继续处理这批 hook。

调用关系:append_hook_events 在真正展开 hook 配置前会先问它一句“这批能不能收”。它是 hook 发现流程里的第一道小门禁。

调用图:被 1 处调用(append_hook_events)。

discover_handlers63–174 ↗
fn discover_handlers(
    config_layer_stack: Option<&ConfigLayerStack>,
    plugin_hook_sources: Vec<PluginHookSource>,
    plugin_hook_load_warnings: Vec<String>,
    bypass_hook_trust: bool,
) -> D

作用:这是本文件的总入口,用来汇总所有配置来源里的 hook,并整理成可执行列表和展示列表。调用者只要问它一次,就能拿到当前环境下所有可用 hook。

数据流:输入是配置层栈、插件 hook、插件加载警告和是否绕过信任检查的开关 → 它读取 hook 状态,检查企业/系统策略,依次扫描受管理要求、各层配置文件、hooks.json、TOML hooks 和插件 hooks → 输出 DiscoveryResult,里面有真正会执行的 handlers、用于展示的 hook_entries,以及所有警告。

调用关系:它是整个发现流程的调度中心。它会把受管理要求交给 append_managed_requirement_handlers,把插件交给 append_plugin_hook_sources,把普通配置读出来后交给 append_hook_events 继续拆解。

调用图:调用 8 个内部函数(hook_states_from_stack, append_hook_events, append_managed_requirement_handlers, append_plugin_hook_sources, config_toml_source_path, hook_metadata_for_config_layer_source, load_hooks_json, load_toml_hooks_from_layer);被 9 处调用(new, allow_managed_hooks_only_false_keeps_unmanaged_hooks, allow_managed_hooks_only_in_config_toml_does_not_enable_policy, allow_managed_hooks_only_keeps_managed_requirement_and_config_layer_hooks, trusted_plugin_hook_stack, unknown_requirement_source_hooks_stay_managed, user_disablement_does_not_filter_managed_layer_hooks, user_disablement_filters_non_managed_hooks_but_not_managed_hooks, list_hooks);外部调用 4 个(new, new, new, format!)。

append_managed_requirement_handlers176–207 ↗
fn append_managed_requirement_handlers(
    handlers: &mut Vec<ConfiguredHandler>,
    hook_entries: &mut Vec<HookListEntry>,
    warnings: &mut Vec<String>,
    display_order: &mut i64,
    config_la

作用:把“受管理要求”里声明的 hook 加进发现结果。这里的受管理 hook 通常来自系统、企业或管理员策略,默认更可信。

数据流:输入是正在累积的 handlers、hook_entries、warnings、显示顺序、配置层栈、hook 状态和发现策略 → 它从 requirements 里取 managed_hooks,推断来源路径和来源类型 → 把这些 hook 交给 append_hook_events,追加到结果里。

调用关系:discover_handlers 会先调用它处理管理员要求里的 hook。它不自己逐条解析命令,而是准备好来源信息后,把细活交给 append_hook_events。

调用图:调用 4 个内部函数(requirements, append_hook_events, hook_source_for_requirement_source, managed_hooks_source_path);被 1 处调用(discover_handlers);外部调用 1 个(new)。

append_plugin_hook_sources209–259 ↗
fn append_plugin_hook_sources(
    handlers: &mut Vec<ConfiguredHandler>,
    hook_entries: &mut Vec<HookListEntry>,
    warnings: &mut Vec<String>,
    display_order: &mut i64,
    plugin_hook_source

作用:把插件提供的 hook 加进发现结果。插件 hook 还会得到一些环境变量,比如插件目录和插件数据目录,方便命令运行时找到自己的文件。

数据流:输入是插件 hook 来源列表和正在累积的结果 → 它逐个插件取出根目录、数据目录、插件 ID、hook 配置,并生成 PLUGIN_ROOT、PLUGIN_DATA 等环境变量 → 再把 hook 配置交给 append_hook_events,追加 handlers、hook_entries 和 warnings。

调用关系:discover_handlers 在处理完配置文件里的 hook 后调用它。它负责把插件世界翻译成本文件统一认识的 HookHandlerSource,然后交给通用解析流程。

调用图:调用 2 个内部函数(plugin_hook_key_source, append_hook_events);被 1 处调用(discover_handlers);外部调用 1 个(new)。

managed_hooks_source_path261–273 ↗
fn managed_hooks_source_path(
    managed_hooks: &ManagedHooksRequirementsToml,
    requirement_source: Option<&RequirementSource>,
) -> AbsolutePathBuf

作用:找出受管理 hook 应该显示成来自哪个路径。优先用当前平台对应的受管理目录,如果没有合适路径,就生成一个兜底路径。

数据流:输入是 managed_hooks 配置和它的来源说明 → 它先问配置有没有当前平台可用的绝对路径,并尝试规范化 → 如果成功就返回这个路径,否则调用 fallback_managed_hooks_source_path 返回一个替代路径。

调用关系:append_managed_requirement_handlers 需要它来给 hook_entries 标明来源。它把“配置从哪里来”这件事整理成一个统一的绝对路径。

调用图:调用 3 个内部函数(managed_dir_for_current_platform, fallback_managed_hooks_source_path, from_absolute_path);被 1 处调用(append_managed_requirement_handlers)。

fallback_managed_hooks_source_path275–301 ↗
fn fallback_managed_hooks_source_path(
    requirement_source: Option<&RequirementSource>,
) -> AbsolutePathBuf

作用:在没有真实受管理 hook 目录时,造一个可读的“虚拟路径”。这样用户看列表时仍然知道 hook 大概来自系统、MDM、企业管理还是未知来源。

数据流:输入是 requirements 的来源信息 → 它按来源类型选择真实文件路径或拼出类似 <mdm:...>/requirements.toml 的虚拟路径 → 输出 AbsolutePathBuf。企业名称和 ID 会先做转义,避免特殊字符把展示弄乱。

调用关系:managed_hooks_source_path 在找不到平台目录时调用它。它还会调用 escape_xml_text 和 synthetic_layer_path 来安全地生成展示路径。

调用图:调用 2 个内部函数(escape_xml_text, synthetic_layer_path);被 1 处调用(managed_hooks_source_path);外部调用 1 个(format!)。

load_hooks_json303–344 ↗
fn load_hooks_json(
    config_folder: Option<&Path>,
    warnings: &mut Vec<String>,
) -> Option<(AbsolutePathBuf, HookEventsToml)>

作用:读取某个配置目录里的 hooks.json,并把它解析成系统能理解的 hook 事件配置。这个函数只负责 JSON 这种老/独立格式。

数据流:输入是配置文件夹路径和 warnings 列表 → 它查找 hooks.json,读取文件内容,解析 JSON,并把文件路径规范成绝对路径 → 如果成功且里面确实有 hook,就返回路径和 hook 配置;如果文件不存在、读取失败或解析失败,就返回 None,并在需要时写入警告。

调用关系:discover_handlers 扫描每一层配置时会调用它。读出来的结果随后会被交给 append_hook_events 继续处理。

调用图:调用 1 个内部函数(from_absolute_path);被 1 处调用(discover_handlers);外部调用 3 个(format!, read_to_string, from_str)。

load_toml_hooks_from_layer346–364 ↗
fn load_toml_hooks_from_layer(
    layer: &ConfigLayerEntry,
    warnings: &mut Vec<String>,
) -> Option<(AbsolutePathBuf, HookEventsToml)>

作用:从某一层 config.toml 里读取 hooks 字段。它让 hook 可以直接写在主配置文件里,而不一定要单独放 hooks.json。

数据流:输入是配置层和 warnings → 它先算出这层配置的来源路径,再从配置内容里拿 hooks 字段并反序列化,也就是把配置文本变成结构化数据 → 如果有有效 hook 就返回路径和配置;解析失败会记警告并返回 None。

调用关系:discover_handlers 扫描配置层时调用它。它依赖 config_toml_source_path 标出来源,解析成功后把结果交给 append_hook_events。

调用图:调用 1 个内部函数(config_toml_source_path);被 1 处调用(discover_handlers);外部调用 2 个(deserialize, format!)。

config_toml_source_path366–386 ↗
fn config_toml_source_path(layer: &ConfigLayerEntry) -> AbsolutePathBuf

作用:给一层配置找出它对应的 config.toml 路径。对于没有真实文件的来源,比如 MDM 或会话参数,它会生成一个虚拟路径用于展示和追踪。

数据流:输入是 ConfigLayerEntry → 它根据来源类型选择系统文件、用户文件、项目 .codex/config.toml,或者为 MDM、企业管理、会话参数生成虚拟路径 → 输出统一的 AbsolutePathBuf。

调用关系:discover_handlers 和 load_toml_hooks_from_layer 都靠它标记 hook 从哪里来。它在真实路径和虚拟路径之间做统一封装。

调用图:调用 2 个内部函数(hooks_config_folder, synthetic_layer_path);被 2 处调用(discover_handlers, load_toml_hooks_from_layer);外部调用 1 个(format!)。

synthetic_layer_path388–398 ↗
fn synthetic_layer_path(path: &str) -> AbsolutePathBuf

作用:把一段虚拟路径文字变成系统内部需要的绝对路径格式。虚拟路径不一定真的存在,只是为了稳定展示来源。

数据流:输入是一段路径字符串 → 在 Windows 上把它挂到 C:\ 下,在其他系统上挂到 / 下 → 输出 AbsolutePathBuf。

调用关系:config_toml_source_path 和 fallback_managed_hooks_source_path 在需要造“看得懂但不一定存在”的路径时会调用它。

调用图:调用 1 个内部函数(resolve_path_against_base);被 2 处调用(config_toml_source_path, fallback_managed_hooks_source_path)。

escape_xml_text400–413 ↗
fn escape_xml_text(value: &str) -> String

作用:把文本里的特殊字符转义,避免它们在虚拟路径展示时造成歧义。比如把 < 变成 &lt;。

数据流:输入是一段普通字符串 → 它逐个字符检查 &, <, >, 引号和单引号,并替换成安全写法 → 输出转义后的字符串。

调用关系:fallback_managed_hooks_source_path 在生成企业管理来源路径时调用它,确保企业名称或 ID 里有特殊字符也不会破坏展示格式。

调用图:被 1 处调用(fallback_managed_hooks_source_path);外部调用 1 个(with_capacity)。

append_hook_events415–439 ↗
fn append_hook_events(
    handlers: &mut Vec<ConfiguredHandler>,
    hook_entries: &mut Vec<HookListEntry>,
    warnings: &mut Vec<String>,
    display_order: &mut i64,
    source: HookHandlerSource<

作用:把一整批按事件分类的 hook 配置展开,并加入总结果。它是从“配置文件格式”走向“逐条 hook 处理”的中间层。

数据流:输入是累积结果、来源信息、一批 HookEventsToml 和策略 → 它先用 HookDiscoveryPolicy::allows 判断来源能不能通过 → 然后把事件配置拆成事件名和匹配组 → 每组交给 append_matcher_groups 继续处理。

调用关系:discover_handlers、append_managed_requirement_handlers 和 append_plugin_hook_sources 都会调用它。它把不同来源统一接到 append_matcher_groups 这条细处理流水线上。

调用图:调用 3 个内部函数(into_matcher_groups, allows, append_matcher_groups);被 3 处调用(append_managed_requirement_handlers, append_plugin_hook_sources, discover_handlers)。

append_matcher_groups441–559 ↗
fn append_matcher_groups(
    handlers: &mut Vec<ConfiguredHandler>,
    hook_entries: &mut Vec<HookListEntry>,
    warnings: &mut Vec<String>,
    display_order: &mut i64,
    source: &HookHandlerSou

作用:这是把 hook 配置变成实际可执行 handler 的核心函数。它会检查匹配规则、处理命令、计算信任状态,并决定这条 hook 是否真的能运行。

数据流:输入是某个事件下的匹配组、来源信息、已有状态和累积结果 → 它为每组算出 matcher,也就是“哪些工具或事件内容会触发它”的规则;跳过无效 matcher、空命令、暂不支持的 async/prompt/agent hook;为命令选择 Windows 或通用版本,设置超时,计算 hash,替换插件环境变量,生成唯一 key,判断启用状态和信任状态 → 总是追加一条用于展示的 HookListEntry;只有启用且可信、受管理或允许绕过信任时,才追加真正会执行的 ConfiguredHandler。

调用关系:append_hook_events 把每个事件的组交给它。它又会调用 command_hook_hash、hook_enabled、hook_trust_status、hook_trusted_hash,以及 matcher_pattern_for_event 和 validate_matcher_pattern 来完成安全判断。

调用图:调用 6 个内部函数(command_hook_hash, hook_enabled, hook_trust_status, hook_trusted_hash, matcher_pattern_for_event, validate_matcher_pattern);被 8 处调用(append_hook_events, bypass_hook_trust_allows_enabled_untrusted_handlers, bypass_hook_trust_respects_disabled_handlers, post_tool_use_keeps_valid_matcher_during_discovery, pre_tool_use_keeps_valid_matcher_during_discovery, pre_tool_use_resolves_windows_command_override_during_discovery, pre_tool_use_treats_star_matcher_as_match_all, user_prompt_submit_ignores_invalid_matcher_during_discovery);外部调用 4 个(cfg!, hook_key, format!, matches!)。

command_hook_hash570–587 ↗
fn command_hook_hash(
    event_name: codex_protocol::protocol::HookEventName,
    matcher: Option<&str>,
    group: &MatcherGroup,
    normalized_handler: HookHandlerConfig,
) -> String

作用:给一条命令 hook 计算一个稳定指纹,用来判断它是不是用户之前信任过的那条。指纹像文件的“身份证号”,内容变了,身份证号也会变。

数据流:输入是事件名、matcher、原始匹配组和规范化后的命令 handler → 它复制匹配组,只保留当前这一个 handler,并把事件名一起序列化成 TOML 值 → 调用 version_for_toml 算出字符串 hash → 输出这个 hash。

调用关系:append_matcher_groups 在判断非受管理 hook 是否可信前会调用它。后面 hook_trust_status 会拿这个当前 hash 和保存过的 trusted_hash 比较。

调用图:被 1 处调用(append_matcher_groups);外部调用 6 个(try_from, version_for_toml, clone, hook_event_key_label, unreachable!, vec!)。

hook_trust_status589–603 ↗
fn hook_trust_status(
    is_managed: bool,
    current_hash: &str,
    trusted_hash: Option<&str>,
) -> HookTrustStatus

作用:判断一条 hook 当前是受管理、可信、被修改过,还是从未信任过。这决定它能不能自动执行。

数据流:输入是是否受管理、当前 hash、保存过的可信 hash → 如果是受管理 hook,直接输出 Managed;否则如果保存 hash 和当前 hash 一样,输出 Trusted;如果有保存 hash 但不一样,输出 Modified;如果没有保存 hash,输出 Untrusted。

调用关系:append_matcher_groups 用它给每条 HookListEntry 标状态,并据此决定要不要把这条 hook 放进可执行 handlers。

调用图:被 1 处调用(append_matcher_groups)。

hook_enabled605–607 ↗
fn hook_enabled(is_managed: bool, state: Option<&HookStateToml>) -> bool

作用:判断一条 hook 当前是否启用。受管理 hook 总是启用;普通 hook 如果用户明确关掉,才算禁用。

数据流:输入是是否受管理和可选的用户状态 → 如果受管理,输出 true;否则检查 state.enabled 是否等于 false → 输出 true 或 false。

调用关系:append_matcher_groups 在生成展示项和可执行项时调用它。它保证用户可以关掉普通 hook,但不能用普通状态关闭管理员强制的 hook。

调用图:被 1 处调用(append_matcher_groups)。

hook_trusted_hash609–613 ↗
fn hook_trusted_hash(is_managed: bool, state: Option<&HookStateToml>) -> Option<&str>

作用:取出普通 hook 保存过的可信指纹。受管理 hook 不需要这个,因为它们天然按管理来源处理。

数据流:输入是是否受管理和可选状态 → 如果是受管理,输出 None;否则从状态里取 trusted_hash 字符串 → 输出可选 hash。

调用关系:append_matcher_groups 用它拿到历史信任记录,然后交给 hook_trust_status 判断当前命令有没有被改过。

调用图:被 1 处调用(append_matcher_groups)。

hook_metadata_for_config_layer_source615–630 ↗
fn hook_metadata_for_config_layer_source(source: &ConfigLayerSource) -> (HookSource, bool)

作用:把配置层来源翻译成 hook 来源标签,并标明它是不是受管理。比如系统配置算 System 且 managed,用户配置算 User 且不 managed。

数据流:输入是 ConfigLayerSource → 它按枚举类型匹配来源,比如 System、User、Project、Mdm、EnterpriseManaged 等 → 输出 HookSource 和 bool,分别表示展示来源和是否受管理。

调用关系:discover_handlers 扫描配置层时调用它。后面的 append_hook_events 和 append_matcher_groups 会依赖这些来源信息做信任和展示判断。

调用图:被 1 处调用(discover_handlers)。

hook_source_for_requirement_source632–652 ↗
fn hook_source_for_requirement_source(source: Option<&RequirementSource>) -> HookSource

作用:把 requirements 的来源翻译成 hook 来源标签。requirements 是管理员或企业策略里的一类配置来源。

数据流:输入是可选 RequirementSource → 它按来源类型返回 Mdm、System、LegacyManagedConfigFile、CloudRequirements 等标签;如果是组合来源,就取第一个贡献来源作为粗略代表 → 输出 HookSource。

调用关系:append_managed_requirement_handlers 调用它给受管理 requirements hook 标来源。它只负责归类,不解析具体 hook。

调用图:被 1 处调用(append_managed_requirement_handlers)。

tests::source_path675–677 ↗
fn source_path() -> AbsolutePathBuf

作用:测试用的小帮手,返回一个固定的假 hooks.json 绝对路径。这样多个测试不用重复写同一段路径构造代码。

数据流:没有业务输入 → 它用测试工具生成 /tmp/hooks.json,并转成绝对路径 → 输出 AbsolutePathBuf。

调用关系:多个测试在构造 HookHandlerSource 或期望结果时调用它,让测试里的来源路径保持一致。

调用图:外部调用 1 个(test_path_buf)。

tests::hook_source679–681 ↗
fn hook_source() -> HookSource

作用:测试用的小帮手,固定返回 System 来源。这样测试可以专注检查 hook 发现逻辑,不被来源选择干扰。

数据流:没有输入 → 直接返回 HookSource::System → 不改动任何状态。

调用关系:tests::hook_handler_source 和部分断言会用它,保证测试里的 managed/system hook 来源一致。

tests::hook_handler_source683–697 ↗
fn hook_handler_source(
        path: &'a AbsolutePathBuf,
        hook_states: &'a std::collections::HashMap<String, HookStateToml>,
    ) -> super::HookHandlerSource<'a>

作用:测试用的小工厂,用来快速造一个“受管理”的 HookHandlerSource。这样测试 append_matcher_groups 时不用搭完整配置层。

数据流:输入是假路径和 hook 状态表 → 它填入路径、key_source、System 来源、is_managed=true、空环境变量等字段 → 输出 HookHandlerSource。

调用关系:多个 append_matcher_groups 相关测试调用它,模拟系统或管理员来源的 hook。

调用图:调用 1 个内部函数(display);外部调用 2 个(hook_source, new)。

tests::unmanaged_hook_handler_source699–714 ↗
fn unmanaged_hook_handler_source(
        path: &'a AbsolutePathBuf,
        hook_states: &'a std::collections::HashMap<String, HookStateToml>,
        bypass_hook_trust: bool,
    ) -> super::HookHan

作用:测试用的小工厂,用来造一个“非受管理”的用户 hook 来源。它还可以设置是否绕过信任检查。

数据流:输入是假路径、hook 状态表和 bypass_hook_trust 开关 → 它填入 User 来源、is_managed=false、传入的绕过开关和空环境变量 → 输出 HookHandlerSource。

调用关系:信任绕过相关测试调用它,用来验证普通 hook 在未信任、禁用等状态下会不会进入可执行列表。

调用图:调用 1 个内部函数(display);外部调用 1 个(new)。

tests::composite_requirement_hook_source_uses_primary_source717–734 ↗
fn composite_requirement_hook_source_uses_primary_source()

作用:验证组合 requirements 来源会使用第一个来源作为代表来源。这样合并策略时,展示标签有一个稳定的归属。

数据流:输入是测试里手写的 Composite 来源,其中第一个是系统 requirements,第二个是企业管理 → 调用 hook_source_for_requirement_source → 断言结果是 HookSource::System。

调用关系:它直接测试 hook_source_for_requirement_source 的组合来源分支,防止以后改动时误把来源归类规则改坏。

调用图:外部调用 2 个(assert_eq!, vec!)。

tests::enterprise_managed_synthetic_path_escapes_display_fields737–749 ↗
fn enterprise_managed_synthetic_path_escapes_display_fields()

作用:验证企业管理来源生成虚拟路径时,会正确转义名称和 ID 里的特殊字符。这样展示路径不会被 <、&、引号等字符弄乱。

数据流:输入是带特殊字符的企业管理来源 → 调用 fallback_managed_hooks_source_path → 把路径转成字符串后检查里面有转义文本,并且没有原始危险片段。

调用关系:它覆盖 fallback_managed_hooks_source_path 和 escape_xml_text 的配合,确保企业来源路径展示安全。

调用图:外部调用 2 个(assert!, fallback_managed_hooks_source_path)。

tests::command_group751–762 ↗
fn command_group(matcher: Option<&str>) -> MatcherGroup

作用:测试用的小帮手,快速创建一个包含 echo hello 命令的 matcher group。可以选择给它加 matcher。

数据流:输入是可选 matcher 字符串 → 它构造 MatcherGroup,里面放一条 command hook,命令是 echo hello,超时等字段留空 → 输出这个组。

调用关系:多个 append_matcher_groups 测试调用它,避免每个测试重复写完整 hook 配置。

调用图:外部调用 1 个(vec!)。

tests::user_prompt_submit_ignores_invalid_matcher_during_discovery765–797 ↗
fn user_prompt_submit_ignores_invalid_matcher_during_discovery()

作用:验证 UserPromptSubmit 事件在发现阶段会忽略无效 matcher,而不是把整条 hook 判错。这个事件本身不需要用这个 matcher 去过滤工具名。

数据流:测试构造一个 matcher 为 '[' 的命令组,这通常不是合法匹配表达式 → 调用 append_matcher_groups → 断言没有警告,并且生成了一个 matcher 为 None 的 handler。

调用关系:它测试 append_matcher_groups 与 matcher_pattern_for_event 的行为,确认某些事件会丢弃不适用的 matcher。

调用图:调用 1 个内部函数(append_matcher_groups);外部调用 6 个(new, assert_eq!, hook_handler_source, source_path, new, vec!)。

tests::pre_tool_use_keeps_valid_matcher_during_discovery800–832 ↗
fn pre_tool_use_keeps_valid_matcher_during_discovery()

作用:验证 PreToolUse 事件会保留合法 matcher。PreToolUse 是工具调用前触发,matcher 用来限定哪些工具会触发 hook。

数据流:测试构造 matcher 为 ^Bash$ 的命令组 → 调用 append_matcher_groups → 断言没有警告,生成的 handler 里保留了这个 matcher。

调用关系:它覆盖 append_matcher_groups 对工具类事件 matcher 的正常路径,防止匹配条件被错误丢掉。

调用图:调用 1 个内部函数(append_matcher_groups);外部调用 6 个(new, assert_eq!, hook_handler_source, source_path, new, vec!)。

tests::bypass_hook_trust_allows_enabled_untrusted_handlers835–862 ↗
fn bypass_hook_trust_allows_enabled_untrusted_handlers()

作用:验证打开“绕过 hook 信任检查”时,未信任但启用的普通 hook 也能进入可执行列表。这个开关通常用于特殊运行模式或测试。

数据流:测试构造一个没有 trusted_hash 的用户 hook 来源,并把 bypass_hook_trust 设为 true → 调用 append_matcher_groups → 断言生成了 handler 和展示项,展示项状态是 Untrusted 且 enabled=true。

调用关系:它测试 append_matcher_groups、hook_trust_status 和绕过信任开关之间的关系。

调用图:调用 1 个内部函数(append_matcher_groups);外部调用 6 个(new, assert_eq!, source_path, unmanaged_hook_handler_source, new, vec!)。

tests::bypass_hook_trust_respects_disabled_handlers865–898 ↗
fn bypass_hook_trust_respects_disabled_handlers()

作用:验证即使绕过信任检查,用户明确禁用的普通 hook 仍然不会执行。也就是说,绕过信任不等于绕过启用开关。

数据流:测试构造一个状态表,把指定 hook key 的 enabled 设为 false,并开启 bypass_hook_trust → 调用 append_matcher_groups → 断言没有可执行 handler,但展示项仍存在,状态为 Untrusted 且 enabled=false。

调用关系:它测试 append_matcher_groups 对 hook_enabled 的尊重,确保禁用状态优先于信任绕过。

调用图:调用 1 个内部函数(append_matcher_groups);外部调用 7 个(new, assert_eq!, format!, source_path, unmanaged_hook_handler_source, from, vec!)。

tests::pre_tool_use_treats_star_matcher_as_match_all901–921 ↗
fn pre_tool_use_treats_star_matcher_as_match_all()

作用:验证 PreToolUse 事件里的 '*' matcher 会保留下来,表示匹配全部。星号在这里相当于“所有工具都算”。

数据流:测试构造 matcher 为 '' 的命令组 → 调用 append_matcher_groups → 断言生成了一个 handler,并且 matcher 仍然是 ''。

调用关系:它测试 matcher 校验和 append_matcher_groups 的配合,确保常用的全匹配写法不会被误判为非法。

调用图:调用 1 个内部函数(append_matcher_groups);外部调用 6 个(new, assert_eq!, hook_handler_source, source_path, new, vec!)。

tests::post_tool_use_keeps_valid_matcher_during_discovery924–945 ↗
fn post_tool_use_keeps_valid_matcher_during_discovery()

作用:验证 PostToolUse 事件会保留合法 matcher。PostToolUse 是工具调用后触发,matcher 用来限定哪些工具调用完成后触发 hook。

数据流:测试构造 matcher 为 Edit|Write 的命令组 → 调用 append_matcher_groups → 断言生成的 handler 事件名是 PostToolUse,并且 matcher 被保留。

调用关系:它覆盖 append_matcher_groups 对工具后置事件的正常匹配逻辑。

调用图:调用 1 个内部函数(append_matcher_groups);外部调用 6 个(new, assert_eq!, hook_handler_source, source_path, new, vec!)。

tests::toml_hook_discovery_ignores_malformed_state_entries948–978 ↗
fn toml_hook_discovery_ignores_malformed_state_entries()

作用:验证 config.toml 里 hook state 写坏时,不会影响正常 hook 的读取。state 是保存启用/信任状态的区域,坏掉不应该让 hook 配置整体报废。

数据流:测试构造一个 TOML 值,其中 hooks.state 的 enabled 写成了字符串,同时还有一个合法 SessionStart hook → 调用 load_toml_hooks_from_layer → 断言没有警告,并且合法 hook 被成功读出。

调用关系:它直接测试 load_toml_hooks_from_layer 的容错行为,确保状态字段错误不会阻断 hook 发现。

调用图:调用 1 个内部函数(new);外部调用 5 个(new, assert_eq!, test_path_buf, config_with_malformed_state_and_session_start_hook, load_toml_hooks_from_layer)。

tests::pre_tool_use_resolves_windows_command_override_during_discovery981–1017 ↗
fn pre_tool_use_resolves_windows_command_override_during_discovery()

作用:验证发现 hook 时会根据操作系统选择正确命令。在 Windows 上用 command_windows,否则用通用 command。

数据流:测试构造一条同时带 command 和 command_windows 的 PreToolUse hook → 调用 append_matcher_groups → 根据当前编译平台断言 handler.command 是 echo windows 或 echo unix。

调用关系:它覆盖 append_matcher_groups 里选择平台命令的逻辑,确保跨平台配置按预期生效。

调用图:调用 1 个内部函数(append_matcher_groups);外部调用 6 个(new, assert_eq!, hook_handler_source, source_path, new, vec!)。

tests::config_with_malformed_state_and_session_start_hook1019–1036 ↗
fn config_with_malformed_state_and_session_start_hook() -> TomlValue

作用:测试用的小帮手,生成一份带坏 state 但带合法 SessionStart hook 的配置值。它专门服务于 TOML 容错测试。

数据流:没有外部输入 → 它用 JSON 宏拼出一段配置,再转成 TomlValue → 输出这份测试配置。

调用关系:tests::toml_hook_discovery_ignores_malformed_state_entries 调用它来准备测试数据。

调用图:外部调用 2 个(from_value, json!)。

tests::hook_metadata_for_config_layer_source_discards_source_details1039–1092 ↗
fn hook_metadata_for_config_layer_source_discards_source_details()

作用:验证配置层来源会被正确归类成粗粒度 hook 来源,并判断是否受管理。测试名字里的“丢弃细节”意思是:文件路径、profile 等细节不影响来源分类。

数据流:测试构造 System、User、Project、Mdm、EnterpriseManaged、SessionFlags、LegacyManaged 等多种来源 → 分别调用 hook_metadata_for_config_layer_source → 断言每种来源得到预期的 HookSource 和 managed 标记。

调用关系:它直接保护 hook_metadata_for_config_layer_source 的映射表,避免以后新增或修改来源时破坏安全分类。

调用图:外部调用 2 个(assert_eq!, test_path_buf)。

hooks/src/engine/command_runner.rs源码 ↗
io_transporthook execution

这个文件像一个“安全的命令执行员”。系统有些 hook 不是写死在程序里的,而是用户配置的一段外部命令;这个文件负责选好用哪个 shell(命令解释器,比如 sh 或 cmd),把输入的 JSON 写进命令的标准输入,再等待它结束。它会同时收集标准输出、标准错误、退出码、开始和结束时间、耗时。如果命令启动不了、输入写不进去、运行报错,或者超过配置的超时时间,它不会让程序一直挂着,而是返回一份带 error 的结果。这里的重要点是它把“不可靠的外部程序”包在一个可控的盒子里:有工作目录、有环境变量、有超时、有输出记录,方便上层判断这个 hook 到底发生了什么。

函数细节3
run_command24–101 ↗
async fn run_command(
    shell: &CommandShell,
    handler: &ConfiguredHandler,
    input_json: &str,
    cwd: &Path,
) -> CommandRunResult

作用:运行一个已经配置好的外部 hook 命令,并把它的输出、退出码、耗时和错误情况整理成一份结果。别人会用它来安全地调用用户脚本,而不是直接裸跑命令。

数据流:进去的是 shell 配置、handler 配置、要喂给命令的 JSON 字符串,以及运行目录。它先记录开始时间,再让 build_command 拼出真正要执行的命令;然后设置工作目录,打开标准输入、标准输出、标准错误这些管道,把 JSON 写给子进程,最后在规定秒数内等待结束。出来的是 CommandRunResult:里面有开始和结束时间、耗时、退出码、stdout、stderr,以及可能的错误信息;如果启动失败、写入失败、等待失败或超时,就返回没有退出码但带 error 的结果。

调用关系:它是上层 execute_handlers 真正执行每个 hook 时会调用的核心步骤。它自己不决定命令长什么样,而是先把这件事交给 build_command;命令跑完后,它把外部世界的复杂情况整理成一个统一结果,再交还给 execute_handlers 做后续判断。

调用图:调用 1 个内部函数(build_command);被 1 处调用(execute_handlers);外部调用 8 个(from_secs, now, piped, from_utf8_lossy, new, now, format!, timeout)。

build_command103–117 ↗
fn build_command(shell: &CommandShell, handler: &ConfiguredHandler) -> Command

作用:根据 shell 配置和 handler 配置,拼出一个 tokio 可以启动的命令对象。简单说,它决定“用什么程序来执行这段命令,以及带哪些参数和环境变量”。

数据流:进去的是 shell 设置和 handler 设置。它先看 shell.program 是否为空:为空就用 default_shell_command 选系统默认 shell;不为空就直接用配置里的程序。然后把 handler.command 放进去,并在自定义 shell 时追加 shell.args,最后把 handler.env 里的环境变量也塞进命令。出来的是一个还没启动、但已经准备好的 Command。

调用关系:它只被 run_command 调用,位置在真正启动子进程之前。它遇到没有明确 shell 的情况,会把选择默认 shell 的工作交给 default_shell_command;拼好后再让 run_command 去设置输入输出管道、工作目录和超时。

调用图:调用 1 个内部函数(default_shell_command);被 1 处调用(run_command);外部调用 1 个(new)。

default_shell_command119–135 ↗
fn default_shell_command() -> Command

作用:在用户没有指定 shell 程序时,帮系统选一个合适的默认 shell。这样配置可以更简单,不用每次都写明该用 sh、bash 或 cmd。

数据流:进去没有显式参数,但它会读取操作系统环境变量。Windows 上先看 COMSPEC,没有就用 cmd.exe,并加上 /C;非 Windows 上先看 SHELL,没有就用 /bin/sh,并加上 -lc。出来的是一个已经带好基础参数的 Command,后面可以继续追加要执行的具体命令。

调用关系:它只在 build_command 发现 shell.program 为空时被调用。它负责处理不同操作系统的差异,让 build_command 不必关心 Windows 和类 Unix 系统该用哪套默认命令格式。

调用图:被 1 处调用(build_command);外部调用 2 个(new, var)。

hooks/src/engine/output_parser.rs源码 ↗
io_transporthook command completion

这里的“钩子”可以理解成系统在某些关键时刻请外部小程序插一脚,比如会话开始、工具使用前后、用户提交提示词、停止运行等。外部程序只能把结果打印成 JSON 字符串,主程序需要有人来判断:这是空输出、乱写的文本,还是一份合格指令?本文件就是这个翻译员。它先用 parse_json 把文本变成结构化数据,再按不同事件分别解释,比如是否继续、是否拦截、拦截理由是什么、能否修改工具输入。它特别在意“拒绝但不给理由”这类危险情况,因为主程序不能只知道被拦了,却不知道为什么。UniversalOutput 保存所有钩子都可能返回的通用指令,各个具体 Output 再加上自己事件专用的信息。

函数细节29
parse_session_start93–100 ↗
fn parse_session_start(stdout: &str) -> Option<SessionStartOutput>

作用:解析“会话开始”钩子的输出。有人启动新会话后,如果外部钩子补充了上下文,这个函数负责把那段内容取出来。

数据流:输入是一段标准输出文本 stdout → 它先用 parse_json 尝试把文本读成会话开始的 JSON 结构,再把通用字段和 additional_context 交给 session_start_output → 输出一个 SessionStartOutput;如果文本为空、不是 JSON 或格式不对,就输出 None。

调用关系:它在 parse_completed 处理钩子运行完成时被调用。自己不直接拼结果,而是把通用收尾工作交给 session_start_output。

调用图:调用 2 个内部函数(parse_json, session_start_output);被 1 处调用(parse_completed)。

parse_subagent_start102–109 ↗
fn parse_subagent_start(stdout: &str) -> Option<SessionStartOutput>

作用:解析“子代理开始”钩子的输出。子代理可以理解成主程序派出去做一部分事的小助手,这个函数读取它启动时钩子返回的补充上下文。

数据流:输入是子代理启动钩子的 stdout → 先用 parse_json 解析成专门的 wire 结构,也就是贴近 JSON 原样的中间格式 → 再抽出通用输出和 additional_context,交给 session_start_output → 输出 SessionStartOutput 或 None。

调用关系:它也由 parse_completed 在钩子结束后调用,并和 parse_session_start 共用 session_start_output,避免两种“开始”事件写两套相同的整理逻辑。

调用图:调用 2 个内部函数(parse_json, session_start_output);被 1 处调用(parse_completed)。

session_start_output111–119 ↗
fn session_start_output(
    universal: HookUniversalOutputWire,
    additional_context: Option<String>,
) -> SessionStartOutput

作用:把“开始类钩子”的原始字段整理成内部统一的 SessionStartOutput。它让会话开始和子代理开始可以共用同一套结果格式。

数据流:输入是通用输出 wire 和可选的 additional_context → 它用 UniversalOutput::from 把通用字段转成内部结构,再把补充上下文放进去 → 输出一个干净的 SessionStartOutput。

调用关系:parse_session_start 和 parse_subagent_start 都把最后组装结果的工作交给它;它再依赖 UniversalOutput::from 做通用字段转换。

调用图:调用 1 个内部函数(from);被 2 处调用(parse_session_start, parse_subagent_start)。

parse_pre_tool_use121–182 ↗
fn parse_pre_tool_use(stdout: &str) -> Option<PreToolUseOutput>

作用:解析“工具使用前”钩子的输出。这个阶段很关键,因为钩子可以在工具真正执行前拦下它,或者在允许时改写工具输入。

数据流:输入是 stdout → 它先 parse_json,再转出通用输出;然后判断钩子用了新版 hookSpecificOutput 还是旧版 decision/reason 写法;接着检查有没有不支持的通用指令、有没有缺少拒绝理由、有没有错误地修改输入 → 最后输出 PreToolUseOutput,里面可能包含拦截理由、补充上下文、更新后的输入,或 invalid_reason。

调用关系:parse_completed 在处理工具前钩子的完成结果时调用它。它先用 UniversalOutput::from 统一通用字段,再用 unsupported_pre_tool_use_universal 做基础合法性检查;后续还会根据新旧两种输出格式继续判断。

调用图:调用 3 个内部函数(from, parse_json, unsupported_pre_tool_use_universal);被 1 处调用(parse_completed)。

parse_permission_request184–205 ↗
fn parse_permission_request(stdout: &str) -> Option<PermissionRequestOutput>

作用:解析“权限请求”钩子的输出。它决定外部钩子是否允许某个需要审批的动作继续,或者拒绝并给出原因。

数据流:输入是权限请求钩子的 stdout → parse_json 把它读成结构;UniversalOutput::from 转换通用字段;再检查通用字段和权限决策里是否使用了当前不支持的保留字段 → 如果合法,就把 allow/deny 变成 PermissionRequestDecision;如果不合法,就清空决策并写入 invalid_reason → 输出 PermissionRequestOutput 或 None。

调用关系:它由 parse_completed 在权限钩子完成时调用,也被本文件里的三个测试直接调用。它会先调用 unsupported_permission_request_universal 做通用检查,再进一步处理钩子专用决策。

调用图:调用 3 个内部函数(from, parse_json, unsupported_permission_request_universal);被 4 处调用(permission_request_rejects_reserved_interrupt_field, permission_request_rejects_reserved_updated_input_field, permission_request_rejects_reserved_updated_permissions_field, parse_completed)。

parse_post_tool_use207–239 ↗
fn parse_post_tool_use(stdout: &str) -> Option<PostToolUseOutput>

作用:解析“工具使用后”钩子的输出。工具已经跑完后,钩子可以选择阻止后续流程继续,但必须说明原因。

数据流:输入是 stdout → parse_json 读出原始结构;UniversalOutput::from 转换通用部分;检查是否用了不支持的 suppressOutput 或更新工具输出字段;再看 decision 是否为 block,并确认 block 时 reason 不是空白 → 输出 PostToolUseOutput,包含是否真正拦截、原因、补充上下文和错误说明。

调用关系:parse_completed 在工具后钩子结束时调用它。它会借助 invalid_block_message 生成“拦截但没理由”的错误提示,并用 unsupported_post_tool_use_universal 做通用限制检查。

调用图:调用 4 个内部函数(from, invalid_block_message, parse_json, unsupported_post_tool_use_universal);被 1 处调用(parse_completed);外部调用 1 个(matches!)。

parse_pre_compact241–248 ↗
fn parse_pre_compact(stdout: &str) -> Option<PreCompactOutput>

作用:解析“压缩前”钩子的输出。压缩通常是把上下文变短前的步骤,这里只需要读取通用指令。

数据流:输入是 stdout → parse_json 尝试解析成 PreCompactCommandOutputWire → UniversalOutput::from 转换通用输出 → 返回 PreCompactOutput,并把 invalid_reason 设为 None。

调用关系:它由 parse_pre_completed 调用,属于压缩前事件的专用入口。它只依赖 parse_json 和 UniversalOutput::from,没有额外拦截规则。

调用图:调用 2 个内部函数(from, parse_json);被 1 处调用(parse_pre_completed)。

parse_post_compact250–257 ↗
fn parse_post_compact(stdout: &str) -> Option<StatelessHookOutput>

作用:解析“压缩后”钩子的输出。它把压缩完成后外部钩子给出的通用指令读进来。

数据流:输入是 stdout → parse_json 解析为 PostCompactCommandOutputWire → UniversalOutput::from 转成内部通用输出 → 返回 StatelessHookOutput,invalid_reason 固定为空。

调用关系:这个函数没有在给出的调用图里显示调用者,但它和其他 parse_* 函数一样,是某个钩子事件完成后读取输出的解析入口。

调用图:调用 2 个内部函数(from, parse_json)。

parse_user_prompt_submit259–281 ↗
fn parse_user_prompt_submit(stdout: &str) -> Option<UserPromptSubmitOutput>

作用:解析“用户提交提示词”钩子的输出。它允许钩子在用户输入真正进入后续流程前拦截,并附带解释。

数据流:输入是 stdout → parse_json 读取 JSON;判断 decision 是否是 block;如果要拦截却没有非空 reason,就生成 invalid_block_reason;再取出 possible additional_context,并转换通用输出 → 输出 UserPromptSubmitOutput。

调用关系:parse_completed 在用户提示词提交钩子结束时调用它。它用 invalid_block_message 统一生成缺少拦截理由时的错误文案,也用 UniversalOutput::from 处理通用字段。

调用图:调用 3 个内部函数(from, invalid_block_message, parse_json);被 1 处调用(parse_completed);外部调用 1 个(matches!)。

parse_stop283–291 ↗
fn parse_stop(stdout: &str) -> Option<StopOutput>

作用:解析“停止”钩子的输出。程序准备停止时,钩子可以要求暂时阻止停止,但必须给理由。

数据流:输入是 stdout → parse_json 读成 StopCommandOutputWire → 把通用字段、decision、reason 和事件名 Stop 一起交给 stop_output → 得到 StopOutput 或 None。

调用关系:它由 parse_completed 在 Stop 事件完成后调用。具体的拦截规则不在这里写,而是复用 stop_output。

调用图:调用 2 个内部函数(parse_json, stop_output);被 1 处调用(parse_completed)。

parse_subagent_stop293–301 ↗
fn parse_subagent_stop(stdout: &str) -> Option<StopOutput>

作用:解析“子代理停止”钩子的输出。它和普通停止类似,只是事件发生在子代理身上。

数据流:输入是 stdout → parse_json 解析为 SubagentStopCommandOutputWire → 抽出通用字段、decision、reason,并带上事件名 SubagentStop 调用 stop_output → 返回 StopOutput 或 None。

调用关系:parse_completed 在子代理停止钩子完成时调用它。它把共同逻辑交给 stop_output,避免和 parse_stop 重复。

调用图:调用 2 个内部函数(parse_json, stop_output);被 1 处调用(parse_completed)。

stop_output303–325 ↗
fn stop_output(
    universal: HookUniversalOutputWire,
    decision: Option<BlockDecisionWire>,
    reason: Option<String>,
    event_name: &str,
) -> StopOutput

作用:组装停止类事件的最终输出,并检查“拦截停止但不给理由”这种不合格情况。

数据流:输入是通用 wire、可选 decision、可选 reason 和事件名 → 它判断 decision 是否为 block;如果 block 但 reason 缺失或只是空白,就用 invalid_block_message 生成错误;然后转换通用输出 → 输出 StopOutput,其中 should_block 只有在拦截理由合格时才为 true。

调用关系:parse_stop 和 parse_subagent_stop 都调用它。它再调用 UniversalOutput::from 转换通用字段,并在需要时调用 invalid_block_message 生成统一错误。

调用图:调用 2 个内部函数(from, invalid_block_message);被 2 处调用(parse_stop, parse_subagent_stop);外部调用 1 个(matches!)。

UniversalOutput::from328–335 ↗
fn from(value: HookUniversalOutputWire) -> Self

作用:把靠近 JSON 字段名的通用输出结构,转换成系统内部使用的 UniversalOutput。它是所有钩子共用字段的“翻译器”。

数据流:输入是 HookUniversalOutputWire,里面有 continue、stop_reason、suppress_output、system_message → 它逐项搬到 UniversalOutput 对应字段里,特别把 JSON 里的 continue 映射成 Rust 里的 continue_processing → 输出 UniversalOutput。

调用关系:几乎所有解析函数都会用到它,包括会话开始、工具前后、权限请求、压缩、用户提示词和停止类事件。它让上层不用关心 wire 结构的字段细节。

调用图:被 8 处调用(parse_permission_request, parse_post_compact, parse_post_tool_use, parse_pre_compact, parse_pre_tool_use, parse_user_prompt_submit, session_start_output, stop_output)。

parse_json338–351 ↗
fn parse_json(stdout: &str) -> Option<T>

作用:把钩子打印出来的文本安全地解析成指定类型的 JSON。它是所有 parse_* 函数前面的第一道门。

数据流:输入是 stdout 字符串 → 先去掉首尾空白;如果为空就返回 None;再用 serde_json 解析成通用 JSON 值;如果顶层不是对象也返回 None;最后再转换成调用者要求的具体结构 → 成功返回 Some,失败返回 None。

调用关系:parse_session_start、parse_pre_tool_use、parse_permission_request、parse_post_tool_use、parse_pre_compact、parse_post_compact、parse_stop、parse_subagent_start 等解析入口都依赖它。它把“是不是一份像样 JSON”这个共同判断集中在一处。

调用图:被 10 处调用(parse_permission_request, parse_post_compact, parse_post_tool_use, parse_pre_compact, parse_pre_tool_use, parse_session_start, parse_stop, parse_subagent_start, parse_subagent_stop, parse_user_prompt_submit);外部调用 2 个(from_str, from_value)。

looks_like_json353–356 ↗
fn looks_like_json(stdout: &str) -> bool

作用:快速判断一段输出看起来是不是 JSON。它不做完整解析,只看开头是不是 { 或 [。

数据流:输入是 stdout → 去掉开头空白 → 检查第一个有效字符是否为左花括号或左方括号 → 输出 true 或 false。

调用关系:parse_completed 和 parse_pre_completed 会用它先做粗略判断。它像门口保安先看外形,真正验内容还要交给 parse_json。

调用图:被 7 处调用(parse_completed, parse_pre_completed, parse_completed, parse_completed, parse_completed, parse_completed, parse_completed)。

invalid_block_message358–360 ↗
fn invalid_block_message(event_name: &str) -> String

作用:生成统一的错误提示:某个钩子说要 block,却没有给出非空理由。

数据流:输入是事件名,比如 PostToolUse 或 Stop → 用 format! 拼成固定格式的英文错误句子 → 输出 String。

调用关系:parse_post_tool_use、parse_user_prompt_submit、stop_output 和 unsupported_pre_tool_use_legacy_decision 都会调用它,保证不同事件遇到同类错误时提示风格一致。

调用图:被 4 处调用(parse_post_tool_use, parse_user_prompt_submit, stop_output, unsupported_pre_tool_use_legacy_decision);外部调用 1 个(format!)。

unsupported_pre_tool_use_universal362–372 ↗
fn unsupported_pre_tool_use_universal(universal: &UniversalOutput) -> Option<String>

作用:检查工具使用前钩子的通用字段里有没有当前不允许的指令。

数据流:输入是 UniversalOutput → 如果 continue_processing 为 false、stop_reason 存在,或 suppress_output 为 true,就返回对应错误说明;否则返回 None。

调用关系:parse_pre_tool_use 会先调用它做第一层检查。只有通用字段没问题,后面才有意义继续看工具前钩子的专用决策。

调用图:被 1 处调用(parse_pre_tool_use)。

unsupported_permission_request_universal374–384 ↗
fn unsupported_permission_request_universal(universal: &UniversalOutput) -> Option<String>

作用:检查权限请求钩子的通用字段是否用了不支持的能力。

数据流:输入是 UniversalOutput → 它依次检查 continue_processing、stop_reason、suppress_output → 发现不支持项就返回错误字符串,没有问题就返回 None。

调用关系:parse_permission_request 在解释 allow 或 deny 前调用它。这样可以避免一个权限钩子一边给权限决定,一边又发出当前流程不接受的通用控制指令。

调用图:被 1 处调用(parse_permission_request)。

unsupported_post_tool_use_universal386–392 ↗
fn unsupported_post_tool_use_universal(universal: &UniversalOutput) -> Option<String>

作用:检查工具使用后钩子的通用字段中是否包含不支持的 suppressOutput。

数据流:输入是 UniversalOutput → 如果 suppress_output 为 true,就返回“PostToolUse 不支持 suppressOutput”的错误;否则返回 None。

调用关系:parse_post_tool_use 调用它。工具后钩子可以做拦截等动作,但不能在这里要求压制输出。

调用图:被 1 处调用(parse_post_tool_use)。

unsupported_permission_request_hook_specific_output394–407 ↗
fn unsupported_permission_request_hook_specific_output(
    decision: Option<&PermissionRequestDecisionWire>,
) -> Option<String>

作用:检查权限请求钩子的专用 decision 里是否用了保留但当前不支持的字段。

数据流:输入是可选的 PermissionRequestDecisionWire 引用 → 如果没有 decision,直接返回 None;如果包含 updated_input、updated_permissions 或 interrupt:true,就返回对应错误;否则返回 None。

调用关系:它属于 parse_permission_request 的专用校验环节。文件底部三个测试就是在确认这些保留字段会被拒绝。

permission_request_decision409–422 ↗
fn permission_request_decision(
    decision: &PermissionRequestDecisionWire,
) -> PermissionRequestDecision

作用:把权限请求钩子的原始 allow/deny 决策,转换成内部真正使用的 PermissionRequestDecision。

数据流:输入是 PermissionRequestDecisionWire → 如果 behavior 是 allow,就输出 Allow;如果是 deny,就输出 Deny,并使用清理过的 message;如果 message 为空,就补一个默认拒绝说明。

调用关系:parse_permission_request 在确认输出合法后会用它。它还会借助 trimmed_reason 去掉消息两边空白,并避免空字符串当成有效理由。

unsupported_post_tool_use_hook_specific_output424–432 ↗
fn unsupported_post_tool_use_hook_specific_output(
    output: &crate::schema::PostToolUseHookSpecificOutputWire,
) -> Option<String>

作用:检查工具使用后钩子的专用输出中是否尝试修改 MCP 工具输出。MCP 可以理解成外部工具协议,这里暂时不允许钩子改它的结果。

数据流:输入是 PostToolUseHookSpecificOutputWire → 如果 updated_mcp_tool_output 存在,就返回不支持的错误;否则返回 None。

调用关系:它是 parse_post_tool_use 的专用校验之一。通用字段检查完后,还需要它确认钩子没有碰当前不支持的专用字段。

unsupported_pre_tool_use_hook_specific_output434–475 ↗
fn unsupported_pre_tool_use_hook_specific_output(
    output: &crate::schema::PreToolUseHookSpecificOutputWire,
) -> Option<String>

作用:检查工具使用前钩子的新版专用输出是否合法。重点是:只有明确允许时才能改工具输入,拒绝时必须给出非空理由。

数据流:输入是 PreToolUseHookSpecificOutputWire → 它先看 updated_input 是否在没有 permissionDecision:allow 时出现;再分别处理 allow、ask、deny 和没有 decision 的情况 → 返回错误说明或 None。

调用关系:parse_pre_tool_use 在使用新版 hookSpecificOutput 时会走到这类检查。它在 deny 但理由为空时调用 invalid_pre_tool_use_reason_message。

调用图:调用 1 个内部函数(invalid_pre_tool_use_reason_message);外部调用 1 个(matches!)。

unsupported_pre_tool_use_legacy_decision477–500 ↗
fn unsupported_pre_tool_use_legacy_decision(
    decision: Option<&PreToolUseDecisionWire>,
    reason: Option<&str>,
) -> Option<String>

作用:检查工具使用前钩子的旧版 decision/reason 写法是否合法。

数据流:输入是旧版 decision 和 reason → 如果 decision 是 approve,返回不支持错误;如果是 block,就要求 reason 非空;如果没有 decision 但给了 reason,也返回错误;否则返回 None。

调用关系:parse_pre_tool_use 在没有使用新版专用决策时调用它。它会复用 invalid_block_message 来生成旧版 block 缺少理由时的错误。

调用图:调用 1 个内部函数(invalid_block_message)。

invalid_pre_tool_use_reason_message502–505 ↗
fn invalid_pre_tool_use_reason_message() -> String

作用:生成工具使用前钩子拒绝但没有给 permissionDecisionReason 时的固定错误提示。

数据流:没有输入 → 直接返回一段固定英文错误字符串 → 供上层放进 invalid_reason。

调用关系:unsupported_pre_tool_use_hook_specific_output 在遇到 permissionDecision:deny 但理由为空时调用它。

调用图:被 1 处调用(unsupported_pre_tool_use_hook_specific_output)。

trimmed_reason507–514 ↗
fn trimmed_reason(reason: &str) -> Option<String>

作用:清理理由字符串,并判断它是不是真的有内容。它防止只写几个空格也被当成“有理由”。

数据流:输入是一段 reason 字符串 → 去掉前后空白 → 如果剩下为空,返回 None;否则返回清理后的 String。

调用关系:多个决策解析和校验逻辑都会用它,比如权限拒绝消息、工具前拒绝理由、旧版 block reason。它是小而关键的文字清洗工具。

tests::permission_request_rejects_reserved_updated_input_field524–544 ↗
fn permission_request_rejects_reserved_updated_input_field()

作用:测试权限请求钩子如果返回 updatedInput,会被判为不支持。这个字段目前是保留字段,不能悄悄生效。

数据流:输入是测试里构造的一段 JSON → 调用 parse_permission_request 解析 → 检查 parsed.invalid_reason 是否等于预期错误文案。

调用关系:这是测试代码,不参与正式运行。它专门验证 parse_permission_request 以及相关校验逻辑没有放过 updatedInput。

调用图:调用 1 个内部函数(parse_permission_request);外部调用 2 个(assert_eq!, json!)。

tests::permission_request_rejects_reserved_updated_permissions_field547–567 ↗
fn permission_request_rejects_reserved_updated_permissions_field()

作用:测试权限请求钩子如果返回 updatedPermissions,会被拒绝并给出明确错误。

数据流:输入是测试构造的 JSON,里面 decision 为 allow 但包含 updatedPermissions → parse_permission_request 解析后 → assert_eq! 确认 invalid_reason 是“不支持 updatedPermissions”。

调用关系:这是测试用例,用来保护 unsupported_permission_request_hook_specific_output 的行为,防止以后改代码时误把保留字段放行。

调用图:调用 1 个内部函数(parse_permission_request);外部调用 2 个(assert_eq!, json!)。

tests::permission_request_rejects_reserved_interrupt_field570–590 ↗
fn permission_request_rejects_reserved_interrupt_field()

作用:测试权限请求钩子如果返回 interrupt:true,会被判为不支持。

数据流:输入是包含 interrupt:true 的测试 JSON → 调用 parse_permission_request → 检查解析结果里的 invalid_reason 是否是预期的 unsupported interrupt:true。

调用关系:这是测试代码。它和另外两个权限请求测试一起,覆盖权限 decision 里三个当前不能用的保留字段。

调用图:调用 1 个内部函数(parse_permission_request);外部调用 2 个(assert_eq!, json!)。

hooks/src/output_spill.rs源码 ↗
io_transportrequest handling / cross-cutting

钩子可以理解成系统在某些时机自动运行的小脚本或小检查。它们有时会吐出很多文字,如果全部塞给模型看,会浪费上下文空间,甚至把真正重要的信息挤掉。这个文件做的事就像“行李超重托运”:短输出直接带上车,长输出先完整存到操作系统的临时目录里,再给模型一张“摘要小票”,上面有开头/结尾预览和完整文件的位置。核心部件是 HookOutputSpiller。它知道输出该放到哪里,也知道每段文字是否超过大约 2500 个 token。token 可以粗略理解成模型读文字时的计量单位。写文件失败时,它不会让流程崩掉,而是退一步,只返回截断后的文本,并记录警告。

函数细节6
HookOutputSpiller::new20–25 ↗
fn new() -> Self

作用:创建一个用来“托运”超长钩子输出的对象。它会提前决定这些临时文件应该放在哪个目录下面。

数据流:进去时不需要外部参数 → 它读取操作系统的临时目录,把它和固定的 hook_outputs 子目录拼起来 → 出来一个 HookOutputSpiller,里面记着之后保存超长输出的根目录。

调用关系:这是使用 HookOutputSpiller 前的准备步骤。后面的 maybe_spill_text、maybe_spill_texts 和 maybe_spill_prompt_fragments 都依赖这个对象里保存的输出目录。

调用图:调用 1 个内部函数(resolve_path_against_base);被 1 处调用(new);外部调用 1 个(temp_dir)。

HookOutputSpiller::maybe_spill_text33–61 ↗
async fn maybe_spill_text(&self, thread_id: ThreadId, text: String) -> String

作用:检查一段钩子输出是不是太长。短的原样返回,长的写进临时文件,然后返回一段缩略版加完整文件路径。

数据流:进去的是一个线程编号和一段文本 → 它先估算文本 token 数;如果没超限,就直接把原文交回去;如果超限,就生成保存路径、创建目录、写入完整文本 → 成功时出来的是“截断预览 + 完整文件路径”,失败时出来的是普通截断文本,并留下警告日志。

调用关系:这是本文件最核心的单条文本处理函数。批量文本处理的 HookOutputSpiller::maybe_spill_texts 会反复调用它,处理提示片段的 HookOutputSpiller::maybe_spill_prompt_fragments 也会把每个片段交给它。它自己会请 hook_output_path 生成文件名,并请 spilled_hook_output_preview 生成模型能看到的替代文本。

调用图:调用 2 个内部函数(hook_output_path, spilled_hook_output_preview);被 3 处调用(maybe_spill_text, maybe_spill_prompt_fragments, maybe_spill_texts);外部调用 6 个(approx_token_count, formatted_truncate_text, create_dir_all, write, Tokens, warn!)。

HookOutputSpiller::maybe_spill_texts63–73 ↗
async fn maybe_spill_texts(
        &self,
        thread_id: ThreadId,
        texts: Vec<String>,
    ) -> Vec<String>

作用:一次处理多段普通文本。它把每段文字都按同一套规则检查:短的保留,长的保存到文件并替换成预览。

数据流:进去的是线程编号和一组文本 → 它先准备一个同样大小的结果列表,然后逐条调用 HookOutputSpiller::maybe_spill_text → 出来的是一组处理后的文本,顺序和原来一致。

调用关系:当上层流程手里不是一段输出,而是一批输出时,会用它来省事。它不自己判断长短,而是把真正的判断和保存工作交给 HookOutputSpiller::maybe_spill_text。

调用图:调用 1 个内部函数(maybe_spill_text);被 1 处调用(maybe_spill_texts);外部调用 1 个(with_capacity)。

HookOutputSpiller::maybe_spill_prompt_fragments75–88 ↗
async fn maybe_spill_prompt_fragments(
        &self,
        thread_id: ThreadId,
        fragments: Vec<HookPromptFragment>,
    ) -> Vec<HookPromptFragment>

作用:处理一组要放进提示词里的钩子片段。它只替换片段里的文本,不丢掉片段原本带着的钩子运行编号。

数据流:进去的是线程编号和一组 HookPromptFragment,也就是带有 text 和 hook_run_id 的小块提示内容 → 它逐个取出 text,交给 HookOutputSpiller::maybe_spill_text 判断是否需要保存到文件 → 出来的是一组新的片段,文本可能变成预览,但 hook_run_id 保持不变。

调用关系:它服务于构造模型提示词的流程。因为提示片段除了文字还有身份信息,所以它在调用 HookOutputSpiller::maybe_spill_text 后,会把原来的 hook_run_id 再装回去,保证后续还能知道这段内容来自哪次钩子运行。

调用图:调用 1 个内部函数(maybe_spill_text);被 1 处调用(maybe_spill_prompt_fragments);外部调用 1 个(with_capacity)。

hook_output_path91–95 ↗
fn hook_output_path(output_dir: &AbsolutePathBuf, thread_id: ThreadId) -> AbsolutePathBuf

作用:给一份要保存的超长钩子输出生成一个独一无二的文件路径。这样不同线程、不同输出不会互相覆盖。

数据流:进去的是输出根目录和线程编号 → 它把根目录、线程编号子目录、一个随机生成的 txt 文件名拼在一起 → 出来的是完整保存路径。

调用关系:HookOutputSpiller::maybe_spill_text 在确定文本太长后会调用它。它只负责“取名字和定位置”,不负责创建目录,也不负责写文件。

调用图:调用 1 个内部函数(join);被 1 处调用(maybe_spill_text);外部调用 2 个(format!, to_string)。

spilled_hook_output_preview101–107 ↗
fn spilled_hook_output_preview(text: &str, path: &AbsolutePathBuf) -> String

作用:生成模型最终能看到的替代文本:一段被截短的预览,再加上完整输出保存在哪里。

数据流:进去的是原始长文本和保存路径 → 它先做一个包含文件路径的尾注,并估算尾注会占多少 token;然后把剩余预算留给正文预览 → 出来的是“截断后的正文 + 完整文件路径提示”。

调用关系:HookOutputSpiller::maybe_spill_text 在成功写完完整文件后会调用它。它的重要细节是先给文件路径预留空间,再截断正文,避免加上路径后又超过钩子输出的预算。

调用图:被 1 处调用(maybe_spill_text);外部调用 3 个(approx_token_count, format!, Tokens)。

hooks/src/engine/dispatcher.rs源码 ↗
orchestrationrequest handling / hook dispatch

钩子可以理解成“某件事发生时自动触发的小脚本”,比如工具使用前、工具使用后、会话开始、用户提交提示词时运行一段命令。这个文件先按事件名和匹配规则挑出该运行的处理器;匹配规则就是配置里写的筛选条件,比如只在工具名是 Bash 时触发。它还特别避免一个处理器因为多个别名都匹配而被重复执行。选好之后,execute_handlers 会把这些命令并发跑起来,也就是同时派出去干活;但最后返回结果时仍按配置顺序排列,方便显示稳定。文件还会生成“正在运行”和“已经完成”的摘要,里面记录开始时间、结束时间、耗时、状态、来源等信息。scope_for_event 则把事件归到线程级或回合级,告诉上层这次钩子影响的是整段会话还是当前一轮操作。底部测试覆盖了各种容易出错的匹配场景。

函数细节17
select_handlers27–34 ↗
fn select_handlers(
    handlers: &[ConfiguredHandler],
    event_name: HookEventName,
    matcher_input: Option<&str>,
) -> Vec<ConfiguredHandler>

作用:从一堆已配置的钩子处理器里,挑出当前事件真正该运行的那些。它是给只有一个匹配输入的常见场景用的便捷入口,比如“这次工具名是 Bash”。

数据流:进去的是处理器列表、事件名、可选的匹配文字;它把这一个匹配文字包装成列表形式;出来的是筛选后的处理器列表,原列表不被改动。

调用关系:它自己不做复杂判断,而是把活交给 select_handlers_for_matcher_inputs。很多测试直接调用它来确认停止事件、工具事件、压缩事件、用户提示词事件等场景的筛选结果是否正确;真实运行流程里,上层也会用它先选出要执行的钩子。

调用图:调用 1 个内部函数(select_handlers_for_matcher_inputs);被 19 处调用(compact_hooks_match_trigger, post_tool_use_matches_tool_name, pre_tool_use_matches_tool_name, pre_tool_use_regex_alternation_matches_each_tool_name, pre_tool_use_star_matcher_matches_all_tools, select_handlers_keeps_duplicate_stop_handlers, select_handlers_keeps_overlapping_session_start_matchers, select_handlers_preserves_declaration_order, user_prompt_submit_ignores_matcher, preview_post (+9 more))。

select_handlers_for_matcher_inputs36–68 ↗
fn select_handlers_for_matcher_inputs(
    handlers: &[ConfiguredHandler],
    event_name: HookEventName,
    matcher_inputs: &[&str],
) -> Vec<ConfiguredHandler>

作用:这是实际做筛选的函数。它按事件名和一个或多个匹配输入,决定哪些配置好的处理器应该被触发,并保证同一个处理器最多只选中一次。

数据流:进去的是处理器列表、目标事件名、若干匹配文字;它先丢掉事件名不一致的处理器,再按不同事件决定是否检查 matcher;对于工具类和压缩类事件,会拿 matcher 去匹配输入文字;对于 UserPromptSubmit 和 Stop,则不看 matcher,事件名对了就选;出来的是克隆出来的处理器列表,顺序保持配置里的声明顺序。

调用关系:select_handlers 会把单个输入转给它。更复杂的场景,比如一个工具调用有多个兼容名字时,也会直接调用它。后续的 preview、run 等流程会依赖它先选出候选钩子,再把这些钩子交给执行阶段。

调用图:被 8 处调用(select_handlers, pre_tool_use_aliases_match_once_per_handler, preview, run, preview, run, preview, run);外部调用 1 个(iter)。

running_summary70–87 ↗
fn running_summary(handler: &ConfiguredHandler) -> HookRunSummary

作用:为一个即将运行或正在运行的钩子生成“运行中”的摘要。这个摘要让界面或日志能立刻显示:这个钩子已经开始了,但还没结束。

数据流:进去的是一个配置好的处理器;它读取处理器的事件名、来源、显示顺序、状态提示等信息,生成运行编号,记录当前 UTC 时间作为开始时间,并把状态设为 Running;出来的是 HookRunSummary,结束时间和耗时暂时为空。

调用关系:它会调用处理器的 run_id 生成标识,也会调用 scope_for_event 判断这次钩子属于线程范围还是当前回合范围。它通常在命令真正结束前被上层用来发布“开始跑了”的状态。

调用图:调用 2 个内部函数(run_id, scope_for_event);外部调用 2 个(new, now)。

execute_handlers89–116 ↗
async fn execute_handlers(
    shell: &CommandShell,
    handlers: Vec<ConfiguredHandler>,
    input_json: String,
    cwd: &Path,
    turn_id: Option<String>,
    parse: fn(&ConfiguredHandler, Comman

作用:并发执行一批已经选好的钩子命令,并把每个命令的原始运行结果解析成上层想要的结构。它的关键点是:命令可以同时跑,但最终结果仍按配置顺序返回。

数据流:进去的是命令 shell、处理器列表、要传给命令的 JSON 字符串、当前工作目录、可选的回合编号,以及一个 parse 解析函数;它为每个处理器启动 run_command,把同一份输入传给外部命令;命令完成后用 parse 把结果变成 ParsedHandler,并记录实际完成的先后;最后再按原配置顺序排序后返回 ParsedHandler 列表。

调用关系:它是执行阶段的核心,被 run_pre、run_post 以及多个 run 流程调用。它把真正跑命令的工作交给 run_command,把“怎么解释命令输出”的工作交给调用者传进来的 parse 函数,因此同一个执行框架可以服务不同种类的钩子事件。

调用图:调用 1 个内部函数(run_command);被 8 处调用(run_post, run_pre, run, run, run, run, run, run);外部调用 2 个(new, new)。

completed_summary118–140 ↗
fn completed_summary(
    handler: &ConfiguredHandler,
    run_result: &CommandRunResult,
    status: HookRunStatus,
    entries: Vec<codex_protocol::protocol::HookOutputEntry>,
) -> HookRunSummary

作用:把一个已经跑完的钩子整理成“完成摘要”。这份摘要说明它成功还是失败、花了多久、输出了哪些内容。

数据流:进去的是处理器、命令运行结果、最终状态、输出条目列表;它从运行结果里取开始时间、完成时间和耗时,从处理器里取来源、事件名、显示顺序等信息;出来的是状态已完成的 HookRunSummary。

调用关系:多个 parse_completed 或 parse_pre_completed 解析函数会调用它,把命令执行结果统一包装成协议层能理解的摘要。它也会调用 run_id 和 scope_for_event,保证完成摘要和运行中摘要使用同一套身份与范围规则。

调用图:调用 2 个内部函数(run_id, scope_for_event);被 8 处调用(parse_completed, parse_pre_completed, parse_completed, parse_completed, parse_completed, parse_completed, parse_completed, parse_completed)。

scope_for_event142–154 ↗
fn scope_for_event(event_name: HookEventName) -> HookScope

作用:判断某种钩子事件影响的是整条会话线程,还是当前这一轮操作。这个范围会写进摘要里,帮助上层决定这条状态该挂在哪里显示或保存。

数据流:进去的是事件名;它按固定规则分类:SessionStart 和 SubagentStart 属于 Thread,其他列出的事件属于 Turn;出来的是 HookScope。

调用关系:running_summary 和 completed_summary 都会调用它。也就是说,无论钩子刚开始还是已经结束,范围判断都来自这里,避免两处写出不一致的规则。

调用图:被 2 处调用(completed_summary, running_summary)。

tests::make_handler167–184 ↗
fn make_handler(
        event_name: HookEventName,
        matcher: Option<&str>,
        command: &str,
        display_order: i64,
    ) -> ConfiguredHandler

作用:这是测试用的小工厂,用来快速造出一个假的 ConfiguredHandler。它让每个测试不用重复写一大段相同字段。

数据流:进去的是事件名、可选 matcher、命令字符串、显示顺序;它补上测试固定的超时时间、来源路径、来源类型和空环境变量;出来的是一个可用于筛选测试的 ConfiguredHandler。

调用关系:下面所有测试都靠它准备样本数据。它会调用 test_path_buf 生成测试路径,并用 new 创建空的环境变量表。

调用图:外部调用 2 个(test_path_buf, new)。

tests::select_handlers_keeps_duplicate_stop_handlers187–208 ↗
fn select_handlers_keeps_duplicate_stop_handlers()

作用:验证 Stop 事件里,即使命令内容一样,只要配置了两条处理器,就应该保留两条。这样用户明确写的多个停止钩子不会被系统自作主张合并掉。

数据流:进去的是测试里构造的两个 Stop 处理器;测试调用 select_handlers;出来的 selected 应该有两个元素,并且显示顺序分别是 0 和 1。

调用关系:它调用 make_handler 准备数据,再调用 select_handlers 检查筛选行为。这个测试保护的是 Stop 事件“不按 matcher 去重、不合并重复命令”的规则。

调用图:调用 1 个内部函数(select_handlers);外部调用 2 个(assert_eq!, vec!)。

tests::select_handlers_keeps_overlapping_session_start_matchers211–232 ↗
fn select_handlers_keeps_overlapping_session_start_matchers()

作用:验证 SessionStart 事件里,如果两条不同 matcher 都匹配同一个输入,两条都要运行。也就是说,重叠匹配不是错误,用户配置了几条就尊重几条。

数据流:进去的是两个 SessionStart 处理器,一个匹配 start.*,一个匹配 ^startup$,输入是 startup;select_handlers 筛选后应该返回两个,并保持显示顺序 0、1。

调用关系:它通过 select_handlers 走普通筛选入口。这个测试确认筛选逻辑不会因为两个规则都命中而错误丢掉其中一个处理器。

调用图:调用 1 个内部函数(select_handlers);外部调用 2 个(assert_eq!, vec!)。

tests::compact_hooks_match_trigger235–255 ↗
fn compact_hooks_match_trigger()

作用:验证压缩前钩子会按触发原因来匹配,比如 manual 只触发 manual 的处理器,不会误触发 auto。

数据流:进去的是两个 PreCompact 处理器,matcher 分别是 manual 和 auto,匹配输入是 manual;select_handlers 返回后应该只剩第一条。

调用关系:它调用 select_handlers,覆盖 PreCompact 这类需要看 matcher 输入的事件,确保压缩钩子不会在错误的触发原因下运行。

调用图:调用 1 个内部函数(select_handlers);外部调用 2 个(assert_eq!, vec!)。

tests::pre_tool_use_matches_tool_name258–278 ↗
fn pre_tool_use_matches_tool_name()

作用:验证工具使用前的钩子会按工具名筛选。比如只写给 Bash 的钩子,不应该在 Edit 工具前运行。

数据流:进去的是两个 PreToolUse 处理器,分别匹配 Bash 和 Edit,输入工具名是 Bash;select_handlers 输出应该只有 Bash 那条。

调用关系:它调用 select_handlers 检查 PreToolUse 的基本匹配规则。这个测试保障工具调用前的自动脚本不会误跑到别的工具上。

调用图:调用 1 个内部函数(select_handlers);外部调用 2 个(assert_eq!, vec!)。

tests::post_tool_use_matches_tool_name281–301 ↗
fn post_tool_use_matches_tool_name()

作用:验证工具使用后的钩子同样会按工具名筛选。这样 Bash 结束后的钩子不会因为别的工具也有配置而混在一起运行。

数据流:进去的是两个 PostToolUse 处理器,分别匹配 Bash 和 Edit,输入是 Bash;select_handlers 输出应该只有 Bash 那条。

调用关系:它调用 select_handlers,和 pre_tool_use_matches_tool_name 对应,确认工具前和工具后的筛选规则保持一致。

调用图:调用 1 个内部函数(select_handlers);外部调用 2 个(assert_eq!, vec!)。

tests::pre_tool_use_star_matcher_matches_all_tools304–324 ↗
fn pre_tool_use_star_matcher_matches_all_tools()

作用:验证 matcher 写成 * 时表示匹配所有工具。这个规则让用户可以配置一个“任何工具使用前都运行”的通用钩子。

数据流:进去的是一个 matcher 为 的 PreToolUse 处理器和一个只匹配 Edit 的处理器,输入工具名是 Bash;select_handlers 输出应该只包含 那条。

调用关系:它调用 select_handlers,间接检查 matches_matcher 对通配符的解释是否符合钩子筛选的预期。

调用图:调用 1 个内部函数(select_handlers);外部调用 2 个(assert_eq!, vec!)。

tests::pre_tool_use_regex_alternation_matches_each_tool_name327–342 ↗
fn pre_tool_use_regex_alternation_matches_each_tool_name()

作用:验证 matcher 可以用类似 Edit|Write 的正则写法匹配多个工具名。正则可以理解成一种更灵活的文本匹配规则。

数据流:进去的是一个 matcher 为 Edit|Write 的处理器;测试分别用 Edit、Write、Bash 调用 select_handlers;前两个应该选中,Bash 不应该选中。

调用关系:它多次调用 select_handlers,覆盖同一条规则匹配多个工具名的情况,确认灵活 matcher 能正常工作。

调用图:调用 1 个内部函数(select_handlers);外部调用 2 个(assert_eq!, vec!)。

tests::pre_tool_use_aliases_match_once_per_handler345–387 ↗
fn pre_tool_use_aliases_match_once_per_handler()

作用:验证当同一个工具调用带有多个兼容名字时,一条处理器即使命中多个名字也只运行一次。这能避免一个脚本因为别名重复而被连续触发好几遍。

数据流:进去的是四个 PreToolUse 处理器,其中最后一个 matcher 同时包含 apply_patch、Write、Edit;测试把这三个名字一起作为匹配输入传入 select_handlers_for_matcher_inputs;出来应该是四条处理器,显示顺序为 0、1、2、3,而不是把最后一条重复加入多次。

调用关系:它直接调用 select_handlers_for_matcher_inputs,因为这个场景需要多个匹配输入。这个测试专门保护文件里注释提到的“每个配置处理器只检查并选中一次”的规则。

调用图:调用 1 个内部函数(select_handlers_for_matcher_inputs);外部调用 2 个(assert_eq!, vec!)。

tests::user_prompt_submit_ignores_matcher390–415 ↗
fn user_prompt_submit_ignores_matcher()

作用:验证 UserPromptSubmit 事件会忽略 matcher。即使 matcher 是无效正则,只要事件名对了,也应该选中。

数据流:进去的是两个 UserPromptSubmit 处理器,一个 matcher 看起来正常,一个 matcher 是不完整的 [;select_handlers 在没有匹配输入的情况下仍应返回两条,并保持顺序。

调用关系:它调用 select_handlers,确认 UserPromptSubmit 走的是“事件名对就选”的分支,而不会因为 matcher 无效导致钩子丢失或报错。

调用图:调用 1 个内部函数(select_handlers);外部调用 2 个(assert_eq!, vec!)。

tests::select_handlers_preserves_declaration_order418–446 ↗
fn select_handlers_preserves_declaration_order()

作用:验证筛选结果保持用户配置时的声明顺序。顺序很重要,因为多个钩子可能像流水线一样依次展示或执行。

数据流:进去的是三条 Stop 处理器,命令分别是 first、second、third;select_handlers 输出后,测试检查顺序仍然是 first、second、third。

调用关系:它调用 select_handlers,保护筛选过程中的稳定顺序。这个规则也和 execute_handlers 最后按配置顺序返回结果的设计相呼应。

调用图:调用 1 个内部函数(select_handlers);外部调用 2 个(assert_eq!, vec!)。

生命周期与提示钩子

这些事件处理器覆盖会话和提示的生命周期节点,包括启动、停止调解以及与压缩相关的决策。

hooks/src/events/session_start.rs源码 ↗
domain_logicstartup / session resume / subagent start

可以把这个文件想成“开场检查员”。每当主会话启动、恢复、清空、压缩,或者一个子代理启动时,它会先从已配置的钩子里挑出匹配的命令,把当前会话编号、工作目录、模型名、权限模式等信息打包成 JSON(一种常见的文本数据格式)传给这些命令。命令跑完后,它再解读输出:普通文字会变成给模型看的补充上下文;合法 JSON 可以表达警告、额外上下文或停止原因;看起来像 JSON 但解析失败的输出会被当成错误。一个重要区别是:只有真正的 SessionStart 可以用 continue:false 阻止继续;SubagentStart 只能补上下文,不能中断流程。文件末尾的测试专门确认这些边界行为,避免钩子输出被误判。

函数细节15
SessionStartSource::as_str31–38 ↗
fn as_str(self) -> &'static str

作用:把会话开始的来源转换成固定的小写文字,比如 startup、resume。这样外部钩子收到的是稳定、好理解的字符串,而不是程序内部的枚举值。

数据流:进去的是一个 SessionStartSource 值,表示会话是新启动、恢复、清空还是压缩后开始;函数根据这个值选出对应英文单词;出来的是一段静态字符串,不改动任何状态。

调用关系:它被 StartHookTarget::matcher_input 和 run 间接用来生成匹配条件和输入 JSON。也就是说,调度器挑钩子、外部命令理解启动原因,都靠这个统一的文字表示。

StartHookTarget::event_name64–69 ↗
fn event_name(&self) -> HookEventName

作用:判断这次要触发的是 SessionStart 事件还是 SubagentStart 事件。调度器需要这个名字来从一堆钩子配置里挑出该跑的那些。

数据流:进去的是一个 StartHookTarget,里面要么是主会话开始,要么是子代理开始;函数看它属于哪一种;出来的是对应的 HookEventName,不产生副作用。

调用关系:preview 和 run 都会先调用它,再把结果交给 dispatcher::select_handlers。它相当于给调度器贴上“这次是什么事件”的标签。

StartHookTarget::matcher_input71–76 ↗
fn matcher_input(&self) -> &str

作用:给钩子匹配器提供更细的匹配文字。主会话用启动来源匹配,子代理用代理类型匹配。

数据流:进去的是 StartHookTarget;如果是 SessionStart,就读取 source 并转成 startup、resume 等文字;如果是 SubagentStart,就读取 agent_type;出来的是一段可用于匹配规则的字符串引用。

调用关系:preview 和 run 在挑选钩子时会把它交给 dispatcher::select_handlers。它让配置可以写成“只在恢复会话时运行”或“只在某类子代理启动时运行”。

preview94–106 ↗
fn preview(
    handlers: &[ConfiguredHandler],
    request: &SessionStartRequest,
) -> Vec<HookRunSummary>

作用:提前预览这次会话开始会跑哪些钩子,但不真的执行它们。界面或上层流程可以用它告诉用户“等会儿会运行这些命令”。

数据流:进去的是所有已配置的处理器和一次会话开始请求;函数用事件名和匹配文字筛出相关处理器;出来的是这些处理器的运行摘要列表,原始配置和请求都不被改动。

调用关系:它由 preview_session_start 调用,内部把筛选工作交给 select_handlers,再把每个选中的处理器转换成 running_summary。它是正式运行 run 之前的“点名表”。

调用图:调用 1 个内部函数(select_handlers);被 1 处调用(preview_session_start)。

run108–209 ↗
async fn run(
    handlers: &[ConfiguredHandler],
    shell: &CommandShell,
    request: SessionStartRequest,
    turn_id: Option<String>,
) -> SessionStartOutcome

作用:真正执行会话开始或子代理开始的钩子,并把它们的结果汇总成上层能理解的结论。结论包括:跑了哪些钩子、是否要停止、停止原因、以及要补给模型的上下文。

数据流:进去的是钩子配置列表、命令运行用的 shell、会话请求和可选 turn_id;函数先筛选匹配的钩子,没有匹配就直接返回空结果;有匹配时,它把请求内容序列化成 JSON,交给 execute_handlers 执行命令,再汇总每个命令解析后的状态;出来的是 SessionStartOutcome,同时没有直接修改请求本身。

调用关系:它由 run_session_start 调用,是这个文件的主流程。它先用 select_handlers 找人,再用 SessionStartCommandInput::new 或 SubagentStartCommandInput 组装输入;序列化失败时交给 serialization_failure_hook_events 和 serialization_failure_outcome;命令跑完后由 parse_completed 逐个解释结果,最后用 flatten_additional_contexts 合并上下文。

调用图:调用 7 个内部函数(execute_handlers, select_handlers, flatten_additional_contexts, serialization_failure_hook_events, serialization_failure_outcome, from_path, new);被 1 处调用(run_session_start);外部调用 3 个(new, format!, to_string)。

parse_completed217–335 ↗
fn parse_completed(
    handler: &ConfiguredHandler,
    run_result: CommandRunResult,
    turn_id: Option<String>,
) -> dispatcher::ParsedHandler<SessionStartHandlerData>

作用:解释一个已经跑完的钩子命令到底意味着什么。它把命令的退出码、错误、标准输出,翻译成“完成、失败、停止、警告、上下文”等结构化结果。

数据流:进去的是钩子配置、命令运行结果和可选 turn_id;函数先看有没有运行错误,再看退出码是否为 0;成功时会尝试按对应事件解析 JSON 输出,解析出的 systemMessage 变警告,additionalContext 变模型上下文,SessionStart 的 continue:false 会变成停止;如果输出像 JSON 但不合法,就标成失败;如果只是普通文字,就当作上下文;出来的是 ParsedHandler,里面含完成事件和该钩子的业务数据。

调用关系:它被 execute_handlers 作为回调使用,也被多个测试直接调用验证行为。它会调用 parse_session_start、parse_subagent_start、looks_like_json、append_additional_context 和 completed_summary,是“外部命令输出”进入系统内部语义的翻译关口。

调用图:调用 5 个内部函数(completed_summary, looks_like_json, parse_session_start, parse_subagent_start, append_additional_context);被 5 处调用(continue_false_preserves_context_for_later_turns, invalid_json_like_stdout_fails_instead_of_becoming_model_context, plain_stdout_becomes_model_context, subagent_start_continue_false_is_ignored, subagent_start_plain_stdout_becomes_model_context);外部调用 3 个(new, format!, panic!)。

serialization_failure_outcome337–344 ↗
fn serialization_failure_outcome(hook_events: Vec<HookCompletedEvent>) -> SessionStartOutcome

作用:在钩子输入 JSON 打包失败时,生成一个安全的失败结果。这样系统不会因为无法构造输入而崩掉,而是把失败记录成钩子事件。

数据流:进去的是已经准备好的 HookCompletedEvent 列表,通常说明序列化失败;函数把它包进 SessionStartOutcome;出来的结果表示不停止、不提供停止原因、也不添加上下文。

调用关系:它只被 run 在序列化失败分支调用。run 先用 common::serialization_failure_hook_events 造出错误事件,再交给这个函数包成统一的返回形状。

调用图:被 1 处调用(run);外部调用 1 个(new)。

tests::plain_stdout_becomes_model_context362–385 ↗
fn plain_stdout_becomes_model_context()

作用:测试普通文本输出会不会被正确当成模型上下文。这个行为很重要,因为很多简单钩子只会 echo 一段文字。

数据流:进去的是测试构造的处理器和一个退出码为 0、stdout 为 hello from hook 的运行结果;测试调用 parse_completed;出来的解析结果应包含一条 Context 记录,状态为 Completed,并且不要求停止。

调用关系:它直接调用 parse_completed、handler 和 run_result。它守住了一个基础约定:不是 JSON 的正常输出,不该被丢掉,而要传给模型作为补充信息。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::continue_false_preserves_context_for_later_turns388–421 ↗
fn continue_false_preserves_context_for_later_turns()

作用:测试 SessionStart 钩子输出 continue:false 时,系统既会停止,又会保留它给出的额外上下文。也就是说,停止不等于丢弃有用信息。

数据流:进去的是一段合法 JSON 输出,里面包含 continue:false、stopReason 和 additionalContext;测试调用 parse_completed;出来的结果应显示 should_stop 为 true,状态为 Stopped,同时上下文和停止原因都记录下来。

调用关系:它调用 parse_completed、handler 和 run_result。它验证 parse_completed 对 SessionStart 的特殊规则:只有主会话开始事件可以通过 JSON 要求暂停后续处理。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::invalid_json_like_stdout_fails_instead_of_becoming_model_context424–451 ↗
fn invalid_json_like_stdout_fails_instead_of_becoming_model_context()

作用:测试“看起来像 JSON 但写坏了”的输出不会被当成普通上下文。这样可以尽早暴露钩子脚本的格式错误,而不是把半截 JSON 塞给模型。

数据流:进去的是一段缺失结尾的 JSON 风格 stdout;测试调用 parse_completed;出来的结果应是 Failed,带有 invalid session start JSON output 错误,并且没有额外上下文。

调用关系:它调用 parse_completed、handler 和 run_result。它确认 parse_completed 会先识别坏 JSON,而不是走普通文本兜底逻辑。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::subagent_start_plain_stdout_becomes_model_context454–478 ↗
fn subagent_start_plain_stdout_becomes_model_context()

作用:测试子代理开始钩子的普通文本输出也会变成模型上下文。这样主会话和子代理在简单输出上的体验保持一致。

数据流:进去的是 SubagentStart 类型的测试处理器、一个成功退出的运行结果和 turn-1;测试调用 parse_completed;出来的结果应包含 turn_id、Completed 状态,以及一条 Context 文本。

调用关系:它调用 parse_completed、handler_for 和 run_result。它验证同一套输出解释器不仅服务 SessionStart,也服务 SubagentStart。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler_for, run_result)。

tests::subagent_start_continue_false_is_ignored481–509 ↗
fn subagent_start_continue_false_is_ignored()

作用:测试子代理开始钩子即使输出 continue:false,也不会阻止流程继续。这个规则防止子代理钩子获得本不该有的中断权限。

数据流:进去的是 SubagentStart 处理器和一段带 continue:false 的合法 JSON;测试调用 parse_completed;出来的结果应仍然是 Completed,should_stop 为 false,但 additionalContext 会被保留。

调用关系:它调用 parse_completed、handler_for 和 run_result。它专门验证文件注释里说的关键差异:SubagentStart 只负责注入上下文,不负责停止会话。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler_for, run_result)。

tests::handler511–513 ↗
fn handler() -> ConfiguredHandler

作用:生成一个默认的 SessionStart 测试处理器,减少测试里的重复代码。它像一个测试用的“假钩子配置”。

数据流:进去没有参数;函数调用 handler_for 并指定事件名为 SessionStart;出来的是 ConfiguredHandler 测试对象。

调用关系:它被多个 SessionStart 测试调用,本身把具体构造工作交给 handler_for。这样测试重点可以放在 parse_completed 的行为上,而不是配置对象怎么拼。

调用图:外部调用 1 个(handler_for)。

tests::handler_for515–527 ↗
fn handler_for(event_name: HookEventName) -> ConfiguredHandler

作用:按指定事件名生成测试用的钩子处理器。测试可以用它分别构造 SessionStart 和 SubagentStart 两种场景。

数据流:进去的是 HookEventName;函数填入固定命令、超时时间、来源路径、来源类型、显示顺序和空环境变量;出来的是一个完整的 ConfiguredHandler。

调用关系:它被 tests::handler 以及子代理相关测试调用,并使用 test_path_buf 构造假的配置文件路径。它是测试里的通用夹具,也就是反复使用的准备材料。

调用图:外部调用 2 个(test_path_buf, new)。

tests::run_result529–539 ↗
fn run_result(exit_code: Option<i32>, stdout: &str, stderr: &str) -> CommandRunResult

作用:生成测试用的命令运行结果,方便测试模拟不同 stdout、stderr 和退出码。它避免每个测试都手写一大段 CommandRunResult。

数据流:进去的是可选退出码、标准输出文本和标准错误文本;函数填入固定开始时间、结束时间、耗时,并把输入文本转成字符串;出来的是 CommandRunResult,error 固定为空。

调用关系:它被所有 parse_completed 相关测试调用。测试用它制造“命令成功输出普通文本”“命令成功输出 JSON”“命令输出坏 JSON”等场景。

hooks/src/events/user_prompt_submit.rs源码 ↗
orchestrationrequest handling

可以把这个文件想成一个“发言前安检口”。用户输入一段话后,系统不会立刻交给模型,而是先把会话编号、当前目录、模型名、权限模式、提示词等信息打包成 JSON(一种常见的文本数据格式),交给配置好的钩子命令去运行。钩子可以说“继续”、可以说“停下并给理由”,也可以补充一段上下文给后续模型使用。这个文件还负责把钩子命令的输出翻译成系统能懂的结果:成功、失败、阻止、停止,以及要展示给用户的提示信息。它比较重要,因为没有这层检查,外部策略、审计、提示词增强这些能力就无法在用户提交问题的第一时间介入。

函数细节10
preview49–61 ↗
fn preview(
    handlers: &[ConfiguredHandler],
    _request: &UserPromptSubmitRequest,
) -> Vec<HookRunSummary>

作用:提前看一眼:如果发生 UserPromptSubmit 事件,哪些钩子会被运行。它不真的执行命令,只返回这些钩子的运行摘要,方便界面或上层流程展示“将要做什么”。

数据流:输入是一组已配置的钩子和一次用户提交请求;它只按 UserPromptSubmit 这个事件名筛选匹配的钩子,不读取请求里的具体提示词;输出是一组摘要,表示这些钩子处于“准备运行”的状态,不改动任何实际数据。

调用关系:它被 preview_user_prompt_submit 调用,用在正式执行前的预览阶段。它把筛选工作交给 dispatcher::select_handlers,再把每个选中的钩子交给 dispatcher::running_summary 生成简短说明。

调用图:调用 1 个内部函数(select_handlers);被 1 处调用(preview_user_prompt_submit)。

run63–131 ↗
async fn run(
    handlers: &[ConfiguredHandler],
    shell: &CommandShell,
    request: UserPromptSubmitRequest,
) -> UserPromptSubmitOutcome

作用:真正执行用户提交提示词时的钩子。它把用户这次输入和环境信息交给外部命令,并汇总这些命令是否要求停止、给出的停止理由、以及补充给模型的上下文。

数据流:输入是钩子列表、命令运行用的 shell,以及包含会话、轮次、目录、模型、权限模式和用户提示词的请求;它先筛出 UserPromptSubmit 钩子,再把请求序列化成 JSON,交给 dispatcher::execute_handlers 执行;最后输出 UserPromptSubmitOutcome,里面有每个钩子的完成事件、是否要停止、停止原因,以及合并后的额外上下文。

调用关系:它被 run_user_prompt_submit 调用,是这个事件的主流程。它负责串起筛选钩子、准备输入、执行命令、解析结果这些步骤;每个命令跑完后,具体怎么解释输出则交给 parse_completed。遇到 JSON 打包失败时,它会用 common::serialization_failure_hook_events 生成失败事件,再交给 serialization_failure_outcome 包成最终结果。

调用图:调用 7 个内部函数(execute_handlers, select_handlers, flatten_additional_contexts, serialization_failure_hook_events, serialization_failure_outcome, from_path, from);被 1 处调用(run_user_prompt_submit);外部调用 3 个(new, format!, to_string)。

parse_completed133–265 ↗
fn parse_completed(
    handler: &ConfiguredHandler,
    run_result: CommandRunResult,
    turn_id: Option<String>,
) -> dispatcher::ParsedHandler<UserPromptSubmitHandlerData>

作用:把一个钩子命令跑完后的原始结果,翻译成系统能懂的状态。它判断这个钩子是成功、失败、阻止了用户请求,还是要求停止后续处理。

数据流:输入是钩子配置、命令运行结果,以及当前轮次编号;它查看有没有运行错误、退出码是多少、标准输出和错误输出写了什么;如果输出是约定格式的 JSON,就解析其中的继续/停止/阻止/额外上下文等字段;如果不是 JSON,就可能把普通文本当作额外上下文;输出是 ParsedHandler,里面既有给前端或日志看的完成事件,也有供 run 汇总用的 should_stop、stop_reason 和额外上下文。

调用关系:它是 dispatcher::execute_handlers 执行完每个命令后使用的解析器,也是多个测试直接验证的核心函数。它会调用 output_parser::parse_user_prompt_submit 理解钩子的 JSON 输出,调用 common::append_additional_context 保存额外上下文,最后调用 dispatcher::completed_summary 生成统一格式的运行摘要。

调用图:调用 5 个内部函数(completed_summary, looks_like_json, parse_user_prompt_submit, append_additional_context, trimmed_non_empty);被 4 处调用(claude_block_decision_blocks_processing, claude_block_decision_requires_reason, continue_false_preserves_context_for_later_turns, exit_code_two_blocks_processing);外部调用 2 个(new, format!)。

serialization_failure_outcome267–274 ↗
fn serialization_failure_outcome(hook_events: Vec<HookCompletedEvent>) -> UserPromptSubmitOutcome

作用:当系统连钩子输入都无法打包成 JSON 时,用它生成一个安全的失败结果。这样上层仍然能收到结构完整的返回值,而不是流程直接崩掉。

数据流:输入是一组已经构造好的失败钩子事件;它不再执行任何命令,也不尝试恢复;输出是 UserPromptSubmitOutcome,其中包含这些失败事件,并明确表示不停止用户请求、没有停止理由、没有额外上下文。

调用关系:它只被 run 在序列化失败时调用。run 先让 common::serialization_failure_hook_events 生成具体失败事件,再用这个函数把事件包成 UserPromptSubmitOutcome 返回给上层。

调用图:被 1 处调用(run);外部调用 1 个(new)。

tests::continue_false_preserves_context_for_later_turns292–325 ↗
fn continue_false_preserves_context_for_later_turns()

作用:测试一种特殊情况:钩子说不要继续处理时,额外上下文仍然会被记录下来。它保证“停止”和“保存上下文”这两件事不会互相冲掉。

数据流:输入是测试里伪造的钩子输出 JSON,里面写着 continue 为 false、停止理由是 pause、还有一段额外上下文;测试把它交给 parse_completed;结果应当是状态为 Stopped、should_stop 为 true,同时额外上下文和停止提示都出现在输出记录里。

调用关系:这是 parse_completed 的单元测试。它用 tests::handler 造一个假钩子配置,用 tests::run_result 造一个假命令结果,然后用断言检查解析结果是否符合预期。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::claude_block_decision_blocks_processing328–361 ↗
fn claude_block_decision_blocks_processing()

作用:测试钩子用 decision:block 这种格式表达“阻止本次请求”时,系统会真的停下来。它确保阻止理由和额外上下文都被正确保留。

数据流:输入是测试伪造的标准输出 JSON,里面包含 block 决策、reason 为 slow down,以及额外上下文;parse_completed 解析后,输出应当显示状态为 Blocked、should_stop 为 true、停止理由为 slow down,并记录上下文和反馈信息。

调用关系:这是围绕 parse_completed 的测试之一。它模拟外部钩子命令成功退出并返回阻止决定,确认解析器会把这个决定传给上层流程。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::claude_block_decision_requires_reason364–392 ↗
fn claude_block_decision_requires_reason()

作用:测试钩子如果说要阻止,却不给理由,系统会把它当成错误。这样可以避免用户只看到“被拦了”,却不知道为什么。

数据流:输入是测试伪造的 JSON,里面有 decision:block,但没有有效 reason;parse_completed 处理后,输出应当不是正常阻止,而是 Failed,并生成一条错误信息,同时不设置 should_stop 和 stop_reason。

调用关系:这是 parse_completed 的规则校验测试。它验证 output_parser 解析出的“缺少阻止理由”会被 parse_completed 转成失败状态,而不是被当作一次合法拦截。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::exit_code_two_blocks_processing395–418 ↗
fn exit_code_two_blocks_processing()

作用:测试老式或简单钩子的阻止方式:命令退出码为 2,并在错误输出里写阻止原因。它保证不写 JSON 的钩子也能表达“请拦住这次请求”。

数据流:输入是一个假命令结果:退出码是 2,标准错误里写着 blocked by policy;parse_completed 处理后,输出应当是 Blocked,should_stop 为 true,停止理由就是这段错误输出,并生成反馈条目。

调用关系:这是 parse_completed 的兼容性测试。它不走 JSON 解析路径,而是验证 parse_completed 对退出码 2 和 stderr 的特殊约定是否生效。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::handler420–432 ↗
fn handler() -> ConfiguredHandler

作用:给测试创建一个假的 UserPromptSubmit 钩子配置。这样每个测试不用重复写一大段配置。

数据流:它没有外部输入;内部填好事件名、命令、超时时间、来源路径、显示顺序等字段;输出是一个 ConfiguredHandler,供测试传给 parse_completed 使用。

调用关系:它被多个测试函数调用,是测试里的小工具。它配合 tests::run_result 一起搭出 parse_completed 所需的最小环境。

调用图:外部调用 2 个(test_path_buf, new)。

tests::run_result434–444 ↗
fn run_result(exit_code: Option<i32>, stdout: &str, stderr: &str) -> CommandRunResult

作用:给测试创建一个假的命令运行结果。测试可以用它快速模拟命令成功、失败、输出 JSON、写错误信息等情况。

数据流:输入是退出码、标准输出文本和标准错误文本;它把这些值放进 CommandRunResult,并填上固定的开始时间、结束时间和耗时;输出是一个可交给 parse_completed 的假运行结果。

调用关系:它被多个测试函数调用,用来模拟 dispatcher 执行外部钩子后的返回值。真正生产环境里的 CommandRunResult 来自命令执行器,测试里则用这个函数手工造出来。

hooks/src/events/stop.rs源码 ↗
domain_logic对话轮次或子代理准备停止时

这个文件像一个“收工前的检查员”。当主助手或子代理准备停止时,它会先找出配置里匹配的 Stop 或 SubagentStop 钩子,把当前会话、目录、模型、最后一条助手消息等信息打包成 JSON,交给这些外部命令运行。命令跑完后,它会读返回结果:如果命令说继续,就正常结束;如果说 block,就把原因变成给模型的续写提示,让模型按要求再做一轮;如果说 continue:false,就认为钩子要求真正停止;如果输出格式不对、退出码异常,也会记录成失败事件。多个钩子的结果会合并:停止优先于阻塞;多个阻塞原因会按顺序拼起来。文件后半部分的测试专门确认这些边界情况不会被误判。

函数细节17
StopHookTarget::event_name47–52 ↗
fn event_name(&self) -> HookEventName

作用:把内部的停止目标转换成协议里使用的事件名。简单说,就是告诉后续流程:这次是普通助手停止,还是子代理停止。

数据流:输入是一个 StopHookTarget 值 → 它看这个值是哪一种 → 输出对应的 HookEventName:Stop 或 SubagentStop。

调用关系:preview 和 run 在挑选钩子前会用它确定事件类型。它相当于给调度器递了一张“这次要找哪类钩子”的标签。

StopHookTarget::matcher_input54–59 ↗
fn matcher_input(&self) -> Option<&str>

作用:给钩子匹配器提供额外筛选信息。普通 Stop 没有额外条件;子代理停止时,会拿子代理类型来匹配。

数据流:输入是停止目标 → 如果是普通 Stop,就返回空;如果是 SubagentStop,就返回 agent_type 这段文字 → 后面用它筛选更具体的钩子。

调用关系:preview 和 run 会把它交给 select_handlers。这样配置里的钩子可以只针对某一类子代理生效,而不是所有停止事件都触发。

preview81–93 ↗
fn preview(
    handlers: &[ConfiguredHandler],
    request: &StopRequest,
) -> Vec<HookRunSummary>

作用:提前预览这次停止事件会运行哪些钩子,但不真的执行。用户界面或上层流程可以用它展示“接下来可能会跑这些检查”。

数据流:输入是一组已配置的钩子和一次停止请求 → 它根据事件名和匹配条件筛出会命中的钩子 → 输出这些钩子的运行摘要列表,不改动真实状态。

调用关系:它由 preview_stop 调用,内部把筛选工作交给 dispatcher::select_handlers,再把每个匹配项变成 running_summary。它是正式 run 之前的“看名单”步骤。

调用图:调用 1 个内部函数(select_handlers);被 1 处调用(preview_stop)。

run95–200 ↗
async fn run(
    handlers: &[ConfiguredHandler],
    shell: &CommandShell,
    request: StopRequest,
) -> StopOutcome

作用:真正执行停止钩子,并给出最终结论:是否停止、是否拦住、拦住时该给模型什么提示。它是这个文件的主流程。

数据流:输入是钩子列表、命令运行环境和 StopRequest → 先筛出匹配钩子;没有匹配就直接返回“什么都不做” → 有匹配时,把请求内容按 Stop 或 SubagentStop 组装成 JSON → 调用外部命令执行钩子 → 逐个解析结果 → 合并成 StopOutcome,里面包含钩子事件记录和最终决定。

调用关系:它由 run_stop 调用,是停止事件的执行入口。它把“找钩子”交给 select_handlers,把“跑命令”交给 execute_handlers,把“解析单个命令结果”交给 parse_completed,最后用 aggregate_results 汇总。序列化失败时,它会走 serialization_failure_hook_events 和 serialization_failure_outcome,保证错误也能被记录。

调用图:调用 7 个内部函数(execute_handlers, select_handlers, serialization_failure_hook_events, aggregate_results, serialization_failure_outcome, from_path, from_string);被 1 处调用(run_stop);外部调用 3 个(new, format!, to_string)。

parse_completed202–371 ↗
fn parse_completed(
    handler: &ConfiguredHandler,
    run_result: CommandRunResult,
    turn_id: Option<String>,
) -> dispatcher::ParsedHandler<StopHandlerData>

作用:读懂一个钩子命令跑完后的结果。它把退出码、标准输出、标准错误转换成系统能理解的状态,比如完成、失败、阻塞或停止。

数据流:输入是钩子配置、命令运行结果和可选的 turn_id → 如果命令本身出错,就记为失败;如果退出码是 0,就尝试把标准输出当作停止钩子的 JSON 来解析;如果退出码是 2,就把标准错误当作阻塞原因;其他退出码或无退出码都算失败 → 输出 ParsedHandler,里面既有给前端/日志看的完成事件,也有后续汇总要用的 StopHandlerData。

调用关系:execute_handlers 在每个钩子命令结束后会用它解析结果。它会调用 parse_stop 或 parse_subagent_stop 读 JSON,调用 completed_summary 生成统一的运行记录。测试里的多个用例也直接调用它,确认各种返回格式都按预期处理。

调用图:调用 4 个内部函数(completed_summary, parse_stop, parse_subagent_stop, trimmed_non_empty);被 7 处调用(block_decision_with_blank_reason_fails_instead_of_blocking, block_decision_with_reason_sets_continuation_prompt, block_decision_without_reason_is_invalid, continue_false_overrides_block_decision, exit_code_two_uses_stderr_feedback_only, exit_code_two_without_stderr_does_not_block, invalid_stdout_fails_instead_of_silently_nooping);外部调用 4 个(new, format!, panic!, unreachable!)。

aggregate_results373–407 ↗
fn aggregate_results(
    results: impl IntoIterator<Item = &'a StopHandlerData>,
) -> StopHandlerData

作用:把多个停止钩子的判断合成一个最终判断。它解决的问题是:如果好几个检查员意见不同,系统到底听谁的。

数据流:输入是一批 StopHandlerData → 只要有一个说 should_stop,就最终停止;只有没人要求停止时,才考虑是否阻塞;如果阻塞,会把所有阻塞原因按声明顺序拼成一段文字,并收集对应的续写提示 → 输出一个合并后的 StopHandlerData。

调用关系:run 在所有钩子都执行并解析完后调用它。测试 aggregate_results_concatenates_blocking_reasons_in_declaration_order 用它确认多个阻塞理由会按顺序合并,不会乱序。

调用图:调用 1 个内部函数(join_text_chunks);被 2 处调用(run, aggregate_results_concatenates_blocking_reasons_in_declaration_order);外部调用 3 个(into_iter, iter, new)。

serialization_failure_outcome409–418 ↗
fn serialization_failure_outcome(hook_events: Vec<HookCompletedEvent>) -> StopOutcome

作用:当停止请求没法打包成 JSON 时,生成一个安全的失败结果。这样系统不会因为准备输入失败而误以为钩子同意通过。

数据流:输入是已经构造好的失败钩子事件 → 它创建 StopOutcome,把这些事件放进去,同时把停止、阻塞、续写提示都设为否或空 → 输出这个保守结果。

调用关系:run 在 serde_json::to_string 失败时调用它。前面的 serialization_failure_hook_events 负责做错误事件,这个函数负责把事件包成停止流程需要的最终返回值。

调用图:被 1 处调用(run);外部调用 1 个(new)。

tests::block_decision_with_reason_sets_continuation_prompt439–464 ↗
fn block_decision_with_reason_sets_continuation_prompt()

作用:测试钩子返回“block 并给出原因”时,系统会真的阻塞,并把原因变成给模型继续工作的提示。

数据流:输入是假造的钩子和命令结果,标准输出里有 decision:block 和 reason → 调用 parse_completed → 检查输出数据里 should_block 为真、block_reason 正确、continuation_fragments 也正确生成。

调用关系:它直接测试 parse_completed 的正常阻塞路径。handler 和 run_result 提供假的配置和命令结果,assert_eq! 用来确认结果没有偏差。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::block_decision_without_reason_is_invalid467–483 ↗
fn block_decision_without_reason_is_invalid()

作用:测试钩子只说“block”但不给原因时,系统不能接受。因为没有原因,模型不知道下一步该怎么改。

数据流:输入是假造的 JSON:decision:block 但没有 reason → 调用 parse_completed → 预期输出是默认数据,不阻塞,并且运行状态是 Failed,错误信息说明缺少非空原因。

调用关系:它覆盖 parse_completed 的错误保护逻辑。这样可以防止外部钩子写得不完整,却让系统卡在一个没有提示的阻塞状态。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::continue_false_overrides_block_decision486–508 ↗
fn continue_false_overrides_block_decision()

作用:测试当钩子同时写了“不要继续”和“block”时,“不要继续”优先。也就是说停止指令比阻塞指令更强。

数据流:输入是假造的 JSON,包含 continue:false、stopReason,以及 decision:block → 调用 parse_completed → 结果应该是 should_stop 为真,带 stop_reason,不产生阻塞提示。

调用关系:它验证 parse_completed 的优先级规则。这个规则保证钩子明确要求停止时,不会被同一份输出里的 block 字段误导。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::exit_code_two_uses_stderr_feedback_only511–532 ↗
fn exit_code_two_uses_stderr_feedback_only()

作用:测试老式或简化的阻塞写法:命令退出码为 2,并把反馈写到标准错误里。系统应把这当成阻塞提示。

数据流:输入是假造的命令结果:退出码 2,stdout 随便写,stderr 写“retry with tests” → 调用 parse_completed → 输出 should_block 为真,阻塞原因和续写提示都来自 stderr。

调用关系:它验证 parse_completed 对退出码 2 的兼容处理。这样外部脚本不一定非要输出 JSON,也能用退出码加错误流表达“请继续修改”。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::exit_code_two_without_stderr_does_not_block535–553 ↗
fn exit_code_two_without_stderr_does_not_block()

作用:测试退出码是 2 但没有给反馈文字时,系统不应该盲目阻塞。没有提示的阻塞对用户和模型都没用。

数据流:输入是假造的命令结果:退出码 2,但 stderr 只有空白 → 调用 parse_completed → 输出默认数据,运行状态是 Failed,并记录缺少 continuation prompt 的错误。

调用关系:它检查 parse_completed 的防呆逻辑。这个测试和上一条一起规定了退出码 2 的完整含义:必须带有非空 stderr 才能阻塞。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::block_decision_with_blank_reason_fails_instead_of_blocking556–572 ↗
fn block_decision_with_blank_reason_fails_instead_of_blocking()

作用:测试 block 的 reason 只有空格时也算无效。因为空白文字不能指导模型继续做什么。

数据流:输入是假造的 JSON:decision:block,reason 是空白字符串 → 调用 parse_completed → 输出不阻塞,状态 Failed,错误信息说明需要非空原因。

调用关系:它验证 parse_completed 会调用 trimmed_non_empty 这类去空白检查,而不是只看字段有没有出现。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::invalid_stdout_fails_instead_of_silently_nooping575–591 ↗
fn invalid_stdout_fails_instead_of_silently_nooping()

作用:测试钩子输出了无效 JSON 时,系统会明确报失败,而不是假装什么都没发生。

数据流:输入是假造的命令结果:退出码 0,但 stdout 是“not json” → 调用 parse_completed → 输出默认数据,运行状态 Failed,并给出“无效 stop hook JSON 输出”的错误。

调用关系:它守住 parse_completed 的解析边界。这样外部钩子写坏了时,用户能看到错误,而不是系统悄悄忽略问题。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::aggregate_results_concatenates_blocking_reasons_in_declaration_order594–629 ↗
fn aggregate_results_concatenates_blocking_reasons_in_declaration_order()

作用:测试多个钩子都阻塞时,阻塞原因会按配置声明顺序拼接。顺序稳定很重要,用户看到的反馈才可预测。

数据流:输入是两个手工构造的 StopHandlerData,分别带 first 和 second → 调用 aggregate_results → 检查输出 block_reason 是“first\n\nsecond”,续写片段也按同样顺序保留。

调用关系:它直接测试 aggregate_results 的合并规则。这个测试确保 run 汇总多个钩子结果时,不会把反馈顺序打乱。

调用图:调用 1 个内部函数(aggregate_results);外部调用 2 个(assert_eq!, vec!)。

tests::handler631–643 ↗
fn handler() -> ConfiguredHandler

作用:给测试造一个最小可用的钩子配置。这样每个测试不用重复写一大堆配置字段。

数据流:没有外部输入 → 构造一个事件名为 Stop、命令为 echo hook、超时和来源等字段齐全的 ConfiguredHandler → 返回给测试使用。

调用关系:多个 parse_completed 测试都会调用它。它用 test_path_buf 生成假的来源路径,让测试更像真实配置,但不需要真的读文件。

调用图:外部调用 2 个(test_path_buf, new)。

tests::run_result645–655 ↗
fn run_result(exit_code: Option<i32>, stdout: &str, stderr: &str) -> CommandRunResult

作用:给测试造一个假的命令运行结果。测试可以自由指定退出码、标准输出和标准错误。

数据流:输入是 exit_code、stdout、stderr → 把它们塞进 CommandRunResult,并填上固定的开始时间、结束时间和耗时 → 返回这个假结果。

调用关系:解析相关测试用它模拟外部钩子命令的各种表现。parse_completed 接到这些假结果后,就能在不真的运行命令的情况下被测试。

hooks/src/events/compact.rs源码 ↗
domain_logiccompaction lifecycle

这里的“钩子”可以理解成门口的检查员:系统准备做 compact(把长对话压缩成更短上下文)之前和之后,会给匹配的外部命令一次机会运行。文件先根据事件名和触发原因挑出该跑的命令,再把会话号、轮次、当前目录、模型名等信息打包成 JSON 交给命令。命令跑完后,它会看退出码、错误、标准输出。如果输出的是规定格式的 JSON,就可能产生警告、失败,甚至要求停止后续流程;如果只是普通日志,就会忽略。这样外部脚本既能参与压缩流程,又不会用杂乱输出把主程序搞乱。

函数细节20
preview_pre58–70 ↗
fn preview_pre(
    handlers: &[ConfiguredHandler],
    request: &PreCompactRequest,
) -> Vec<HookRunSummary>

作用:提前看一眼:如果马上要进入压缩前阶段,会有哪些 PreCompact 钩子被执行。它只做预览,不真的运行命令。

数据流:输入是一组已配置的钩子和一次压缩前请求 → 它按事件名 PreCompact 和触发原因筛选匹配项 → 输出每个将要运行的钩子的简短运行摘要,不改动外部状态。

调用关系:上层的 preview_pre_compact 会在需要展示或准备压缩前钩子时调用它;它把筛选工作交给 select_handlers,然后把每个匹配的钩子变成可展示的运行摘要。

调用图:调用 1 个内部函数(select_handlers);被 1 处调用(preview_pre_compact)。

run_pre72–123 ↗
async fn run_pre(
    handlers: &[ConfiguredHandler],
    shell: &CommandShell,
    request: PreCompactRequest,
) -> PreCompactOutcome

作用:真正运行压缩前的钩子命令。它决定压缩是否还能继续,以及如果要停,停下来的原因是什么。

数据流:输入是钩子列表、命令运行用的 shell、以及压缩前请求 → 它先筛选匹配钩子,再把请求转成 JSON,交给外部命令执行,最后解析每个命令的结果 → 输出 PreCompactOutcome,里面有完成事件、是否应该停止、停止原因;如果没有匹配钩子就返回空结果。

调用关系:run_pre_compact 在压缩前流程中调用它。它依次借助 select_handlers 找人、pre_command_input_json 准备输入、execute_handlers 执行命令;如果 JSON 准备失败,就用 serialization_failure_hook_events 生成失败事件。

调用图:调用 4 个内部函数(execute_handlers, select_handlers, serialization_failure_hook_events, pre_command_input_json);被 1 处调用(run_pre_compact);外部调用 2 个(new, format!)。

pre_command_input_json125–138 ↗
fn pre_command_input_json(request: &PreCompactRequest) -> Result<String, serde_json::Error>

作用:把压缩前钩子需要知道的信息打包成 JSON 字符串。外部脚本靠这份 JSON 知道自己正在处理哪个会话、哪一轮、什么模型、什么触发原因。

数据流:输入是 PreCompactRequest → 它取出子代理信息、会话号、轮次、工作目录、转录文件路径等字段,并把路径和可空字符串整理成 schema 规定的样子 → 输出一个 JSON 字符串;如果序列化失败就返回错误。

调用关系:run_pre 在真正执行钩子前调用它来准备标准输入;测试 pre_compact_input_includes_lifecycle_metadata 也调用它,确认关键元数据没有漏掉。

调用图:调用 2 个内部函数(from_path, from);被 2 处调用(run_pre, pre_compact_input_includes_lifecycle_metadata);外部调用 1 个(to_string)。

preview_post140–152 ↗
fn preview_post(
    handlers: &[ConfiguredHandler],
    request: &PostCompactRequest,
) -> Vec<HookRunSummary>

作用:提前看一眼:压缩完成后会有哪些 PostCompact 钩子被执行。它用于预览,不启动任何外部命令。

数据流:输入是一组钩子配置和压缩后请求 → 它按 PostCompact 和触发原因挑出匹配钩子 → 输出这些钩子的运行摘要。

调用关系:preview_post_compact 会在需要展示压缩后钩子计划时调用它;它把匹配工作交给 select_handlers,再把结果转成摘要。

调用图:调用 1 个内部函数(select_handlers);被 1 处调用(preview_post_compact)。

run_post154–205 ↗
async fn run_post(
    handlers: &[ConfiguredHandler],
    shell: &CommandShell,
    request: PostCompactRequest,
) -> StatelessHookOutcome

作用:真正运行压缩后的钩子命令。它让外部脚本在压缩完成后做检查、记录或要求后续流程暂停。

数据流:输入是钩子列表、shell、压缩后请求 → 它筛选 PostCompact 钩子,把请求转成 JSON,运行命令,再解析每个命令的输出 → 输出 StatelessHookOutcome,包含完成事件、是否停止、停止原因;没有匹配钩子时返回空结果。

调用关系:run_post_compact 在压缩后流程中调用它。它使用 select_handlers 找匹配命令,调用 post_command_input_json 准备输入,交给 execute_handlers 执行,并用 parse_post_completed 解析结果。

调用图:调用 4 个内部函数(execute_handlers, select_handlers, serialization_failure_hook_events, post_command_input_json);被 1 处调用(run_post_compact);外部调用 2 个(new, format!)。

post_command_input_json207–220 ↗
fn post_command_input_json(request: &PostCompactRequest) -> Result<String, serde_json::Error>

作用:把压缩后钩子需要的信息打包成 JSON 字符串。这样外部脚本能知道压缩刚发生在哪个会话、哪一轮、什么目录和模型下。

数据流:输入是 PostCompactRequest → 它整理会话、轮次、子代理、路径、目录、事件名、模型和触发原因 → 输出符合 PostCompactCommandInput 格式的 JSON 字符串,或返回序列化错误。

调用关系:run_post 在执行压缩后钩子前调用它;测试 post_compact_input_includes_lifecycle_metadata 用它检查输出 JSON 是否包含生命周期元数据。

调用图:调用 2 个内部函数(from_path, from);被 2 处调用(run_post, post_compact_input_includes_lifecycle_metadata);外部调用 1 个(to_string)。

parse_pre_completed228–313 ↗
fn parse_pre_completed(
    handler: &ConfiguredHandler,
    run_result: CommandRunResult,
    turn_id: Option<String>,
) -> dispatcher::ParsedHandler<CompactHandlerData>

作用:解读一个压缩前钩子命令跑完后的结果。它把退出码、错误和输出翻译成系统能理解的状态,比如成功、失败或停止。

数据流:输入是钩子配置、命令运行结果、轮次 ID → 它先看进程有没有运行错误,再看退出码;成功时会尝试解析 PreCompact JSON 输出,提取警告、停止原因或格式错误 → 输出 ParsedHandler,里面有完成事件和是否应该停止的内部数据。

调用关系:execute_handlers 在 run_pre 的流程里会用它解析每个命令结果;多个测试也直接调用它,验证停止、无效 JSON、普通日志等情况。它会调用 parse_pre_compact、looks_like_json、trimmed_non_empty 和 completed_summary。

调用图:调用 4 个内部函数(completed_summary, looks_like_json, parse_pre_compact, trimmed_non_empty);被 3 处调用(block_decision_is_not_supported_for_pre_compact, continue_false_stops_before_compaction, pre_compact_ignores_plain_stdout);外部调用 1 个(new)。

parse_post_completed315–327 ↗
fn parse_post_completed(
    handler: &ConfiguredHandler,
    run_result: CommandRunResult,
    turn_id: Option<String>,
) -> dispatcher::ParsedHandler<CompactHandlerData>

作用:解读一个压缩后钩子命令跑完后的结果。它本身很薄,只是把 PostCompact 的标签和解析器交给通用解析函数。

数据流:输入是钩子配置、命令运行结果、轮次 ID → 它把这些数据连同 PostCompact 标签传给 parse_completed → 输出统一的 ParsedHandler。

调用关系:run_post 通过 execute_handlers 使用它处理每个压缩后钩子的结果;测试也调用它检查压缩后停止和普通输出被忽略的行为。

调用图:调用 1 个内部函数(parse_completed);被 2 处调用(post_compact_continue_false_stops_after_compaction, post_compact_ignores_plain_stdout)。

parse_completed329–416 ↗
fn parse_completed(
    handler: &ConfiguredHandler,
    run_result: CommandRunResult,
    turn_id: Option<String>,
    event_label: &'static str,
    parse_output: fn(&str) -> Option<output_parser::S

作用:这是压缩后钩子结果解析的通用工具。它把外部命令的各种结局统一翻译成钩子完成事件。

数据流:输入是钩子、命令结果、轮次 ID、事件标签和一个输出解析函数 → 它检查运行错误、退出码、标准输出;遇到合法 JSON 就提取警告或停止要求,遇到像 JSON 但格式不对的输出就标为失败,普通文字输出则忽略 → 输出 ParsedHandler,附带是否停止和停止原因。

调用关系:parse_post_completed 把实际工作交给它。它再调用 completed_summary 生成最终摘要,必要时用 looks_like_json 判断输出是不是疑似 JSON,用 trimmed_non_empty 从错误输出里取可读错误信息。

调用图:调用 3 个内部函数(completed_summary, looks_like_json, trimmed_non_empty);被 1 处调用(parse_post_completed);外部调用 2 个(new, format!)。

tests::pre_compact_input_includes_lifecycle_metadata438–455 ↗
fn pre_compact_input_includes_lifecycle_metadata()

作用:测试压缩前钩子的输入 JSON 是否带齐关键上下文。它防止以后改代码时漏掉会话、轮次、目录、模型或触发原因。

数据流:输入是测试构造的压缩前请求 → 它调用 pre_command_input_json 生成 JSON,再解析回普通 JSON 值 → 用断言确认内容和预期完全一致。

调用关系:这是测试用例,在测试运行时执行;它依赖 pre_request 构造样本请求,并直接验证 pre_command_input_json 的行为。

调用图:调用 1 个内部函数(pre_command_input_json);外部调用 3 个(assert_eq!, pre_request, from_str)。

tests::post_compact_input_includes_lifecycle_metadata458–475 ↗
fn post_compact_input_includes_lifecycle_metadata()

作用:测试压缩后钩子的输入 JSON 是否包含完整的生命周期信息。这样外部脚本不会因为缺少上下文而误判。

数据流:输入是测试构造的压缩后请求 → 它调用 post_command_input_json 生成 JSON,再解析成 JSON 值 → 用断言检查字段和值是否符合预期。

调用关系:这是测试运行时的检查;它使用 post_request 准备样本数据,重点保护 post_command_input_json 的输出格式。

调用图:调用 1 个内部函数(post_command_input_json);外部调用 3 个(assert_eq!, post_request, from_str)。

tests::block_decision_is_not_supported_for_pre_compact478–497 ↗
fn block_decision_is_not_supported_for_pre_compact()

作用:测试 PreCompact 不接受旧式或不支持的 block 决策格式。这样外部钩子不能用错误格式悄悄改变主流程。

数据流:输入是一个退出码为 0、标准输出含有 {"decision":"block"} 的假命令结果 → 它调用 parse_pre_completed 解析 → 期望结果是失败,并给出“无效 PreCompact JSON 输出”的错误条目。

调用关系:这是针对 parse_pre_completed 的测试;它用 handler 和 run_result 构造假钩子与假运行结果,然后用断言锁定当前规则。

调用图:调用 1 个内部函数(parse_pre_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::continue_false_stops_before_compaction500–517 ↗
fn continue_false_stops_before_compaction()

作用:测试压缩前钩子可以用 continue:false 明确叫停压缩。它保证“暂停按钮”真的生效。

数据流:输入是一个成功退出、输出 {"continue":false,"stopReason":"nope"} 的假结果 → parse_pre_completed 解析后 → 应得到 Stopped 状态、should_stop 为 true,并保留停止原因。

调用关系:这是 parse_pre_completed 的行为测试;它通过 handler 和 run_result 造出场景,确认 run_pre 以后汇总停止决定时有可靠数据可用。

调用图:调用 1 个内部函数(parse_pre_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::post_compact_continue_false_stops_after_compaction520–544 ↗
fn post_compact_continue_false_stops_after_compaction()

作用:测试压缩后钩子也能用 continue:false 要求后续流程停下来。它确认压缩完成后的暂停信号不会被忽略。

数据流:输入是一个成功退出、输出停止 JSON 的假 PostCompact 结果 → 它调用 parse_post_completed → 期望状态为 Stopped,should_stop 为 true,并带有 stopReason。

调用关系:这是 parse_post_completed 的测试;parse_post_completed 内部会走 parse_completed,所以这个测试也间接保护通用解析逻辑。

调用图:调用 1 个内部函数(parse_post_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::pre_compact_ignores_plain_stdout547–556 ↗
fn pre_compact_ignores_plain_stdout()

作用:测试压缩前钩子打印普通文字日志时不会被当成错误。这样脚本写日志不会无故破坏压缩流程。

数据流:输入是退出码 0、标准输出为普通文本的假结果 → 调用 parse_pre_completed → 输出应是 Completed,且没有额外消息条目。

调用关系:这是 parse_pre_completed 的测试;它确认只有符合约定的 JSON 才会影响流程,普通 stdout 只是日志。

调用图:调用 1 个内部函数(parse_pre_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::post_compact_ignores_plain_stdout559–568 ↗
fn post_compact_ignores_plain_stdout()

作用:测试压缩后钩子打印普通文字日志时会被忽略。它避免外部脚本的正常日志被误报成失败。

数据流:输入是退出码 0、stdout 为普通文本的假 PostCompact 结果 → 调用 parse_post_completed → 得到 Completed 状态和空消息列表。

调用关系:这是 parse_post_completed 的测试;它同时覆盖 parse_completed 对普通文本输出的处理规则。

调用图:调用 1 个内部函数(parse_post_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::pre_request570–581 ↗
fn pre_request() -> super::PreCompactRequest

作用:构造一个固定的压缩前测试请求。测试用它来避免每个用例都手写一大堆重复字段。

数据流:没有外部输入 → 它创建固定会话 ID、轮次、当前目录、模型名和触发原因 → 返回 PreCompactRequest 测试对象。

调用关系:pre_compact_input_includes_lifecycle_metadata 调用它来生成样本请求;它使用 from_string 生成线程 ID,并用 test_path_buf 准备测试路径。

调用图:调用 1 个内部函数(from_string);外部调用 1 个(test_path_buf)。

tests::post_request583–594 ↗
fn post_request() -> super::PostCompactRequest

作用:构造一个固定的压缩后测试请求。它是测试里的样板数据生成器。

数据流:没有外部输入 → 它填入固定会话 ID、轮次、目录、模型和触发原因 → 返回 PostCompactRequest 测试对象。

调用关系:post_compact_input_includes_lifecycle_metadata 调用它;它和 pre_request 类似,只是事件阶段换成压缩后。

调用图:调用 1 个内部函数(from_string);外部调用 1 个(test_path_buf)。

tests::handler596–608 ↗
fn handler(event_name: HookEventName) -> ConfiguredHandler

作用:构造一个假的钩子配置,供解析测试使用。它让测试不用真的读取用户配置文件。

数据流:输入是一个钩子事件名 → 它填入假命令、超时时间、来源路径、显示顺序和空环境变量 → 返回 ConfiguredHandler。

调用关系:多个解析测试调用它,把 PreCompact 或 PostCompact 的事件名塞进去;随后这些假 handler 会交给 parse_pre_completed 或 parse_post_completed。

调用图:外部调用 2 个(test_path_buf, new)。

tests::run_result610–620 ↗
fn run_result(exit_code: Option<i32>, stdout: &str, stderr: &str) -> CommandRunResult

作用:构造一个假的命令运行结果。测试用它模拟外部脚本成功、失败、输出不同内容等情况。

数据流:输入是退出码、标准输出、标准错误 → 它填入固定开始结束时间和耗时,并把输出转成字符串 → 返回 CommandRunResult,不会真的运行命令。

调用关系:多个测试用它制造不同场景,再交给 parse_pre_completed 或 parse_post_completed,检查解析逻辑是否按预期工作。

工具与权限钩子

这些事件处理器调解工具执行和批准请求,将钩子输出汇总为阻止、上下文和允许/拒绝结果。

hooks/src/events/pre_tool_use.rs源码 ↗
domain_logictool request handling

可以把这个文件想成工具执行前的“安检口”。系统准备调用某个工具时,会先把这次调用的信息整理成一份 JSON 表单,交给匹配到的钩子脚本。脚本可以说“放行”、说“别执行,原因是……”,也可以给模型补一句上下文,或者把工具输入改成新版本。这里的核心流程是:先按工具名和别名挑出该跑的处理器;再序列化输入;然后执行这些处理器;最后把它们的输出翻译成统一结果。一个重要规则是:只要有任意钩子要求阻止,就不再采用输入改写;如果多个钩子都改写了输入,则采用实际最后完成的那个改写。文件后半部分是一组测试,专门确认拦截、改写、旧格式兼容、错误 JSON、退出码等边界情况都按约定工作。

函数细节24
preview54–69 ↗
fn preview(
    handlers: &[ConfiguredHandler],
    request: &PreToolUseRequest,
) -> Vec<HookRunSummary>

作用:提前预览这次工具调用会触发哪些 PreToolUse 钩子,但不真正执行它们。这样界面或日志可以先显示“等会儿会跑哪些检查”。

数据流:输入是一组已配置的处理器和一次工具调用请求 → 它根据工具名和匹配别名生成匹配用的名字列表,再挑出 PreToolUse 事件下会命中的处理器 → 输出每个将要运行的钩子的摘要,并把本次 tool_use_id 放进运行标识里。

调用关系:它被 preview_pre_tool_use 以及相关测试调用,站在真正执行前的位置。它把筛选工作交给 select_handlers_for_matcher_inputs,把工具名整理工作交给 matcher_inputs,自己只负责把结果包装成预览摘要。

调用图:调用 2 个内部函数(select_handlers_for_matcher_inputs, matcher_inputs);被 3 处调用(preview_pre_tool_use, preview_and_completed_run_ids_include_tool_use_id, serialization_failure_run_ids_include_tool_use_id)。

run71–142 ↗
async fn run(
    handlers: &[ConfiguredHandler],
    shell: &CommandShell,
    request: PreToolUseRequest,
) -> PreToolUseOutcome

作用:真正执行工具调用前的所有匹配钩子,并汇总它们的决定。它决定这次工具调用是继续、被拦住、获得额外上下文,还是使用被改写后的输入。

数据流:输入是处理器列表、命令运行环境和完整请求 → 它先找出匹配的钩子;没有匹配就直接返回“放行”;有匹配就把请求转成 JSON,交给执行器运行外部命令,再逐个解析结果 → 输出 PreToolUseOutcome,其中包含完成事件、是否阻止、阻止理由、给模型的补充上下文,以及可能的新工具输入。

调用关系:它由 run_pre_tool_use 在工具执行前调用,是这个文件的主流程。它调用 command_input_json 准备标准输入,调用 execute_handlers 跑脚本,解析时使用 parse_completed,最后用 flatten_additional_contexts 和 latest_updated_input 汇总多个钩子的影响;如果序列化失败,则走 serialization_failure_hook_events_for_tool_use 和 serialization_failure_outcome。

调用图:调用 8 个内部函数(execute_handlers, select_handlers_for_matcher_inputs, flatten_additional_contexts, matcher_inputs, serialization_failure_hook_events_for_tool_use, command_input_json, latest_updated_input, serialization_failure_outcome);被 1 处调用(run_pre_tool_use);外部调用 2 个(new, format!)。

latest_updated_input148–162 ↗
fn latest_updated_input(
    results: &[dispatcher::ParsedHandler<PreToolUseHandlerData>],
) -> Option<Value>

作用:在多个钩子都试图改写工具输入时,选出“实际最后完成”的那个版本。这样结果按完成时间决定,而不是按配置顺序决定。

数据流:输入是一批已经解析好的钩子结果 → 它挑出带 updated_input 的结果,连同完成顺序一起比较 → 输出完成顺序最大的那份 updated_input;如果没人改写,就输出空。

调用关系:它只被 run 使用,发生在所有钩子都执行完之后。run 先确认没有任何钩子阻止执行,再让它挑出最终应采用的改写输入。

调用图:被 1 处调用(run);外部调用 1 个(iter)。

command_input_json170–186 ↗
fn command_input_json(request: &PreToolUseRequest) -> Result<String, serde_json::Error>

作用:把一次 PreToolUse 请求整理成要发给钩子脚本的 JSON 字符串。钩子脚本靠这份 JSON 知道当前会话、工具名、工具参数、目录等信息。

数据流:输入是 PreToolUseRequest → 它提取会话号、轮次、子代理信息、当前目录、模型、权限模式、工具名、工具输入和 tool_use_id,并把可为空的路径处理成约定格式 → 输出一段 JSON 字符串;如果某个字段无法序列化,就返回错误。

调用关系:它被 run 在执行钩子前调用,也被测试 command_input_uses_request_tool_name 单独检查。它特别保证发给脚本的是正式 tool_name,而不是内部匹配别名,避免审计和策略判断混乱。

调用图:调用 2 个内部函数(from_path, from);被 2 处调用(run, command_input_uses_request_tool_name);外部调用 1 个(to_string)。

parse_completed188–303 ↗
fn parse_completed(
    handler: &ConfiguredHandler,
    run_result: CommandRunResult,
    turn_id: Option<String>,
) -> dispatcher::ParsedHandler<PreToolUseHandlerData>

作用:把一个钩子命令的原始运行结果,翻译成系统能理解的完成事件和业务决定。它负责判断成功、失败、阻止、补充上下文和输入改写。

数据流:输入是处理器配置、命令运行结果和可选 turn_id → 它先看命令有没有运行错误,再看退出码;退出码为 0 时解析 stdout 里的 PreToolUse JSON,退出码为 2 时把 stderr 当作阻止理由,其他异常情况记为失败 → 输出 ParsedHandler,里面既有给前端或日志看的完成摘要,也有 should_block、block_reason、additional_contexts_for_model、updated_input 等内部数据。

调用关系:它作为回调交给 execute_handlers,由 run 间接使用;大量测试也直接调用它验证各种边界。它把 JSON 解析交给 parse_pre_tool_use,把疑似坏 JSON 的判断交给 looks_like_json,把摘要生成交给 completed_summary。

调用图:调用 5 个内部函数(completed_summary, looks_like_json, parse_pre_tool_use, append_additional_context, trimmed_non_empty);被 13 处调用(additional_context_is_recorded, deprecated_approve_decision_fails_open, deprecated_block_decision_blocks_processing, deprecated_block_decision_with_additional_context_blocks_processing, exit_code_two_blocks_processing, invalid_json_like_stdout_fails_instead_of_becoming_noop, last_completed_updated_input_wins, permission_decision_allow_can_update_input, permission_decision_allow_without_updated_input_fails_open, permission_decision_deny_blocks_processing (+3 more));外部调用 2 个(new, format!)。

serialization_failure_outcome305–313 ↗
fn serialization_failure_outcome(hook_events: Vec<HookCompletedEvent>) -> PreToolUseOutcome

作用:当准备钩子输入 JSON 失败时,生成一个安全的默认结果。它记录失败事件,但不因为这个内部错误去阻止工具执行。

数据流:输入是一组已经构造好的钩子完成事件 → 它把这些事件放进 PreToolUseOutcome,同时把 should_block 设为 false,把阻止原因、额外上下文和改写输入都设为空 → 输出这份失败情况下的统一结果。

调用关系:它只被 run 在 command_input_json 出错时调用。前一步由 serialization_failure_hook_events_for_tool_use 生成可报告的失败事件,它负责把这些事件包成最终 outcome。

调用图:被 1 处调用(run);外部调用 1 个(new)。

tests::command_input_uses_request_tool_name336–345 ↗
fn command_input_uses_request_tool_name()

作用:测试发给钩子的 JSON 里使用的是请求里的正式工具名。它防止内部匹配名或别名不小心泄漏成实际 tool_name。

数据流:它先造一个请求,再把 tool_name 改成 apply_patch → 调用 command_input_json 生成 JSON,并反序列化回来检查字段 → 如果 JSON 里的 tool_name 不是 apply_patch,测试就失败。

调用关系:这个测试直接覆盖 command_input_json。它模拟 run 执行前的输入准备阶段,确保后续钩子和审计看到稳定、正确的工具名。

调用图:调用 1 个内部函数(command_input_json);外部调用 3 个(assert_eq!, request_for_tool_use, from_str)。

tests::permission_decision_deny_blocks_processing348–376 ↗
fn permission_decision_deny_blocks_processing()

作用:测试新格式的 permissionDecision 为 deny 时,钩子会阻止工具继续执行。它确认阻止理由会被保存并作为反馈展示。

数据流:输入是一段 stdout JSON,表示权限决定为 deny 且理由是 do not run that → 交给 parse_completed 解析 → 得到 should_block 为 true、block_reason 有值、状态为 Blocked,并产生一条反馈记录。

调用关系:它直接验证 parse_completed 对新权限格式的处理。这个场景对应 run 执行钩子后,某个钩子明确说“不许跑”的情况。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::permission_decision_allow_can_update_input379–401 ↗
fn permission_decision_allow_can_update_input()

作用:测试 permissionDecision 为 allow 且带 updatedInput 时,钩子可以改写工具输入。它保证放行时的改写不会被丢掉。

数据流:输入是一段 stdout JSON,表示允许执行,并把 command 改成 echo rewritten → parse_completed 解析后 → 输出状态为 Completed,updated_input 里保存新命令,没有阻止理由。

调用关系:它覆盖 parse_completed 的“放行并改写”路径。这个结果之后会由 run 汇总,并可能通过 latest_updated_input 成为最终工具输入。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::last_completed_updated_input_wins404–430 ↗
fn last_completed_updated_input_wins()

作用:测试多个钩子都改写输入时,最后完成的那个改写胜出。它防止系统错误地按配置顺序选结果。

数据流:它构造两个解析后的钩子结果,并手动设置 completion_order,一个较早完成、一个较晚完成 → 调用 latest_updated_input → 期望得到完成顺序更晚的那份 command。

调用关系:这个测试直接验证 latest_updated_input。它对应 run 汇总所有钩子结果时解决“多个改写冲突”的规则。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::permission_decision_allow_without_updated_input_fails_open433–461 ↗
fn permission_decision_allow_without_updated_input_fails_open()

作用:测试 allow 但没有 updatedInput 的特殊情况会被记为失败,但不会阻止工具执行。这里的“fails open”意思是:报告钩子格式不对,但默认放行。

数据流:输入是一段只写 permissionDecision: allow 的 stdout JSON → parse_completed 尝试解析 → 输出 should_block 为 false、updated_input 为空、状态为 Failed,并记录“不支持 permissionDecision:allow”的错误。

调用关系:它直接检查 parse_completed 对不完整新格式的容错行为。这个行为影响 run 最终是否阻止工具:解析失败会被报告,但不自动拦截。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::deprecated_block_decision_blocks_processing464–492 ↗
fn deprecated_block_decision_blocks_processing()

作用:测试旧格式 decision:block 仍然能阻止工具执行。它保证老用户的钩子脚本不会因为格式升级而失效。

数据流:输入是一段旧格式 JSON,包含 decision 为 block 和 reason → parse_completed 解析 → 输出 should_block 为 true、block_reason 为 reason、状态为 Blocked,并有反馈记录。

调用关系:它验证 parse_completed 的旧格式兼容分支。这个分支让 run 可以同时接受新旧两种钩子输出。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::deprecated_block_decision_with_additional_context_blocks_processing495–529 ↗
fn deprecated_block_decision_with_additional_context_blocks_processing()

作用:测试旧格式 block 决定里同时带额外上下文时,两件事都会生效。也就是既拦住工具,又把补充信息记下来给模型看。

数据流:输入是一段旧格式 block JSON,并在 hookSpecificOutput 里带 additionalContext → parse_completed 解析 → 输出阻止状态、阻止原因和一条额外上下文,同时完成记录里先有 Context 再有 Feedback。

调用关系:它覆盖 parse_completed 里旧格式和额外上下文共同出现的路径。这个结果在 run 汇总时会贡献到 additional_contexts,同时也会让 should_block 变成 true。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::unsupported_permission_decision_fails_open532–560 ↗
fn unsupported_permission_decision_fails_open()

作用:测试不支持的 permissionDecision,比如 ask,会被当成钩子输出错误,但不会拦住工具。这样系统不会把未知格式误判成拒绝。

数据流:输入是一段 permissionDecision 为 ask 的 stdout JSON → parse_completed 解析 → 输出状态 Failed,记录不支持 ask 的错误,同时 should_block 仍为 false。

调用关系:它直接验证 parse_completed 的错误处理。这个场景帮助 run 在面对未来或错误的钩子输出时保持可预测:报告失败,但不擅自阻止。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::deprecated_approve_decision_fails_open563–587 ↗
fn deprecated_approve_decision_fails_open()

作用:测试旧格式 decision:approve 不再被当成有效放行决定,而是记为失败并默认放行。它避免旧的“approve”语义被继续误用。

数据流:输入是一段旧格式 approve JSON → parse_completed 解析 → 输出 should_block 为 false、状态 Failed,并记录“不支持 decision:approve”的错误。

调用关系:它覆盖 parse_completed 对旧格式中不支持值的处理。这个测试保证 run 汇总时不会因为 approve 产生额外行为。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::additional_context_is_recorded590–624 ↗
fn additional_context_is_recorded()

作用:测试钩子返回 additionalContext 时,系统会把它保存下来。这个上下文可以作为额外提示交给模型。

数据流:输入是一段 deny JSON,同时带 additionalContext 为 nope → parse_completed 解析 → 输出 should_block 为 true,并在 additional_contexts_for_model 中保存 nope,完成记录里也包含 Context 和 Feedback。

调用关系:它验证 parse_completed 会调用追加上下文的逻辑。run 之后会把多个钩子的这些上下文压平成一组,交给后续流程使用。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::plain_stdout_is_ignored627–645 ↗
fn plain_stdout_is_ignored()

作用:测试钩子普通打印一行文字时,不会被当成特殊指令。这样脚本随手输出日志不会影响工具执行。

数据流:输入是退出码 0、stdout 为 hook ran successfully 的运行结果 → parse_completed 看到它不是可解析的 PreToolUse JSON → 输出 Completed,且没有阻止、没有错误、没有额外记录。

调用关系:它覆盖 parse_completed 的“普通输出忽略”路径。这个行为让 run 执行普通脚本时更宽容,不会把普通日志误认为控制命令。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::invalid_json_like_stdout_fails_instead_of_becoming_noop648–672 ↗
fn invalid_json_like_stdout_fails_instead_of_becoming_noop()

作用:测试看起来像 JSON 但格式坏掉的输出,会被当成失败,而不是静默忽略。这样钩子脚本写错格式时能被发现。

数据流:输入是退出码 0、stdout 以 JSON 形式开头但不完整 → parse_completed 判断它像 JSON,却无法按 PreToolUse 解析 → 输出状态 Failed,并记录 invalid pre-tool-use JSON output 错误。

调用关系:它验证 parse_completed 结合 looks_like_json 的保护逻辑。这个行为帮助 run 把脚本格式错误报告出来,而不是假装什么都没发生。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::exit_code_two_blocks_processing675–699 ↗
fn exit_code_two_blocks_processing()

作用:测试钩子用退出码 2 表示阻止执行时,stderr 里的文字会成为阻止理由。这给脚本提供了一种不用 JSON 的简单拦截方式。

数据流:输入是 exit_code 为 2、stderr 为 blocked by policy 的运行结果 → parse_completed 读取并修剪 stderr → 输出 should_block 为 true、block_reason 为 blocked by policy、状态为 Blocked。

调用关系:它覆盖 parse_completed 对退出码 2 的特殊约定。run 执行外部钩子后会依赖这个解析结果决定是否拦截工具。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::preview_and_completed_run_ids_include_tool_use_id702–723 ↗
fn preview_and_completed_run_ids_include_tool_use_id()

作用:测试预览阶段和完成阶段生成的运行 ID 都包含 tool_use_id。这样同一个钩子对应哪一次工具调用就不会混淆。

数据流:它先造一个 tool_use_id 为 tool-call-123 的请求 → 调用 preview 得到预览 ID,再调用 parse_completed 并用 hook_completed_for_tool_use 包装完成事件 → 检查完成 ID 和预览 ID 一致且包含 tool_use_id。

调用关系:它同时覆盖 preview 和完成事件包装的配合。这个测试保证 UI 或日志能把“将要运行”和“已经完成”的同一次工具钩子对上号。

调用图:调用 3 个内部函数(hook_completed_for_tool_use, parse_completed, preview);外部调用 4 个(assert_eq!, handler, request_for_tool_use, run_result)。

tests::serialization_failure_run_ids_include_tool_use_id726–739 ↗
fn serialization_failure_run_ids_include_tool_use_id()

作用:测试即使序列化输入失败,生成的失败事件 ID 也会包含 tool_use_id。这样失败报告也能准确指向哪次工具调用。

数据流:它创建请求并先用 preview 得到预期运行 ID → 再调用 serialization_failure_hook_events_for_tool_use 生成序列化失败事件 → 检查失败事件的 run.id 和预览 ID 一致。

调用关系:它验证预览逻辑和序列化失败报告逻辑使用同一套 ID 规则。这个场景对应 run 在 command_input_json 失败后走失败事件分支。

调用图:调用 2 个内部函数(serialization_failure_hook_events_for_tool_use, preview);外部调用 4 个(assert_eq!, handler, request_for_tool_use, vec!)。

tests::handler741–753 ↗
fn handler() -> ConfiguredHandler

作用:构造一个测试用的 PreToolUse 处理器配置。测试们用它假装用户在 hooks.json 里配置了一个匹配 Bash 的钩子。

数据流:它不接收输入 → 固定填入事件名、匹配器、命令、超时时间、来源路径、显示顺序等字段 → 输出一个 ConfiguredHandler,供其他测试传给 preview 或 parse_completed。

调用关系:它被多个测试当作小工厂使用。这样每个测试不用重复写一大段处理器配置,可以专注检查 parse_completed、preview 等函数的行为。

调用图:外部调用 2 个(test_path_buf, new)。

tests::run_result755–765 ↗
fn run_result(exit_code: Option<i32>, stdout: &str, stderr: &str) -> CommandRunResult

作用:构造一个测试用的命令运行结果。测试们用它模拟外部钩子脚本的退出码、标准输出和错误输出。

数据流:输入是可选退出码、stdout 字符串和 stderr 字符串 → 它填上固定的开始时间、结束时间和耗时,并把错误字段设为空 → 输出 CommandRunResult,供 parse_completed 解析。

调用关系:它被大量 parse_completed 测试调用。它扮演命令执行器的替身,让测试不用真的启动外部脚本。

tests::request_for_tool_use767–781 ↗
fn request_for_tool_use(tool_use_id: &str) -> super::PreToolUseRequest

作用:构造一个测试用的 PreToolUse 请求。测试们用它模拟“系统准备调用 Bash 工具执行 echo hello”。

数据流:输入是 tool_use_id → 它生成新的 session_id,填入 turn_id、当前目录、模型名、权限模式、工具名 Bash 和工具输入 JSON → 输出 PreToolUseRequest。

调用关系:它被 preview 和序列化相关测试使用。它提供稳定的请求样本,让 preview、command_input_json 和失败事件 ID 的测试有共同基础。

调用图:调用 1 个内部函数(new);外部调用 3 个(new, test_path_buf, json!)。

hooks/src/events/post_tool_use.rs源码 ↗
domain_logicrequest handling / after tool use

当模型调用完某个工具,比如 Bash 命令或 MCP 工具后,系统不能只看“工具跑完了”就继续往下走。有些用户脚本可能想检查输出是否危险、给模型补一段背景信息,或者直接拦住下一步。这个文件就是这道“事后检查门”。它先根据工具名和别名挑出该运行的处理器,把本次工具调用的信息打包成 JSON 传给脚本;脚本跑完后,再把退出码、标准输出、错误信息翻译成系统能懂的结果:完成、失败、拦截、停止、给模型的反馈、额外上下文等。一个重要细节是:匹配时可以用别名,但传给脚本和记录日志时仍保留原始工具名,方便前后事件对得上。文件里也有测试,专门确认拦截、补充上下文、非法输出、退出码 2 等关键情况不会被误判。

函数细节18
preview53–68 ↗
fn preview(
    handlers: &[ConfiguredHandler],
    request: &PostToolUseRequest,
) -> Vec<HookRunSummary>

作用:提前预览这次工具使用结束后会跑哪些钩子,但不真的执行它们。它像是在正式开工前先列一张“待办清单”。

数据流:输入是一批已配置的处理器和一次工具使用请求 → 它拿工具名和匹配别名生成匹配条件,再筛出 PostToolUse 事件对应的处理器 → 输出一组运行摘要,摘要里会带上这次 tool_use_id,方便之后把预览和真正完成的记录对上。

调用关系:它会被更上层的 preview_post_tool_use 调用,用来展示或记录即将执行的钩子;测试也会调用它,确认预览里的运行编号和真正完成后的编号一致。它把筛选工作交给 dispatcher,把工具匹配输入的准备交给 common。

调用图:调用 2 个内部函数(select_handlers_for_matcher_inputs, matcher_inputs);被 3 处调用(preview_post_tool_use, preview_and_completed_run_ids_include_tool_use_id, serialization_failure_run_ids_include_tool_use_id)。

run70–137 ↗
async fn run(
    handlers: &[ConfiguredHandler],
    shell: &CommandShell,
    request: PostToolUseRequest,
) -> PostToolUseOutcome

作用:真正执行“工具用完后”的钩子,并汇总它们是否要拦截、是否要给模型补充信息、是否要给模型反馈。它是这个文件的主流程。

数据流:输入是处理器列表、命令运行环境 shell、以及本次工具调用的完整请求 → 它先筛出匹配的钩子;如果没有,就直接返回空结果;如果有,就把请求序列化成 JSON,交给 dispatcher 去运行外部命令;最后把每个命令的结果合并成一次 PostToolUseOutcome → 输出包含钩子完成事件、是否阻止继续、额外上下文和反馈消息。

调用关系:它由上层 run_post_tool_use 调用,是工具执行结束后的实际入口。它自己负责串起筛选、输入打包、命令执行和结果汇总;具体运行外部命令交给 dispatcher::execute_handlers,具体解析单个钩子结果交给 parse_completed。

调用图:调用 8 个内部函数(execute_handlers, select_handlers_for_matcher_inputs, flatten_additional_contexts, join_text_chunks, matcher_inputs, serialization_failure_hook_events_for_tool_use, command_input_json, serialization_failure_outcome);被 1 处调用(run_post_tool_use);外部调用 2 个(new, format!)。

command_input_json145–162 ↗
fn command_input_json(request: &PostToolUseRequest) -> Result<String, serde_json::Error>

作用:把一次 PostToolUse 请求整理成要发给钩子脚本的 JSON 字符串。脚本读这个 JSON,就知道刚才哪个工具被调用、输入是什么、结果是什么。

数据流:输入是一份 PostToolUseRequest → 它取出会话、轮次、当前目录、模型名、权限模式、工具名、工具输入和工具响应等字段,并处理可选的子代理信息和 transcript 路径 → 输出一个 JSON 字符串;如果某些内容无法序列化,就返回错误。

调用关系:run 在真正执行钩子前会调用它来准备标准输入。测试 tests::command_input_uses_request_tool_name 会检查一个关键规则:即使匹配时用了别名,传给脚本的 tool_name 也必须是请求里的原始工具名。

调用图:调用 2 个内部函数(from_path, from);被 2 处调用(run, command_input_uses_request_tool_name);外部调用 1 个(to_string)。

parse_completed164–300 ↗
fn parse_completed(
    handler: &ConfiguredHandler,
    run_result: CommandRunResult,
    turn_id: Option<String>,
) -> dispatcher::ParsedHandler<PostToolUseHandlerData>

作用:把一个钩子命令跑完后的原始结果,翻译成系统内部能理解的状态和消息。它决定这次钩子算成功、失败、拦截,还是要求停止。

数据流:输入是处理器配置、命令运行结果,以及可选的 turn_id → 它先看命令本身有没有运行错误,再看退出码、stdout 和 stderr;如果 stdout 是 PostToolUse 支持的 JSON,就解析其中的 block、continue、reason、additionalContext 等字段;如果退出码是 2,则把 stderr 当成拦截反馈;其他异常情况会变成失败记录 → 输出 ParsedHandler,里面有完成事件和额外数据,比如 should_block、给模型的上下文、给模型的反馈。

调用关系:dispatcher::execute_handlers 在每个钩子命令结束后会用它来解释结果。run 再把这些 ParsedHandler 汇总成最终 outcome。大量测试直接调用它,因为这里是判断钩子行为是否正确的核心规则所在。

调用图:调用 5 个内部函数(completed_summary, looks_like_json, parse_post_tool_use, append_additional_context, trimmed_non_empty);被 8 处调用(additional_context_is_recorded, block_decision_stops_normal_processing, continue_false_stops_with_reason, continue_false_without_reason_synthesizes_feedback, exit_two_blocks_with_feedback, plain_stdout_is_ignored_for_post_tool_use, preview_and_completed_run_ids_include_tool_use_id, unsupported_updated_mcp_tool_output_fails_open);外部调用 2 个(new, format!)。

serialization_failure_outcome302–309 ↗
fn serialization_failure_outcome(hook_events: Vec<HookCompletedEvent>) -> PostToolUseOutcome

作用:当钩子输入 JSON 打包失败时,生成一个安全的失败结果。它不会让系统误以为应该拦截,只会记录失败事件。

数据流:输入是一组已经做好的 HookCompletedEvent 失败事件 → 它把这些事件放进 PostToolUseOutcome,并把 should_block 设为 false、额外上下文和反馈都留空 → 输出这个失败 outcome。

调用关系:run 在 command_input_json 出错时会调用它。真正生成失败事件的工作由 common::serialization_failure_hook_events_for_tool_use 做,这个函数只是把事件包装成 PostToolUseOutcome。

调用图:被 1 处调用(run);外部调用 1 个(new)。

tests::command_input_uses_request_tool_name332–341 ↗
fn command_input_uses_request_tool_name()

作用:测试传给钩子脚本的 JSON 里,tool_name 用的是请求里的真实工具名,而不是匹配用的别名。这样日志和前后事件才能对得上。

数据流:它先造一个工具调用请求,并把 tool_name 改成 apply_patch → 调用 command_input_json 得到 JSON,再解析回普通 JSON 值 → 检查里面的 tool_name 是否正好是 apply_patch。

调用关系:这个测试直接保护 command_input_json 的一个重要约定。它用 request_for_tool_use 准备假请求,用断言确认序列化结果没有悄悄改名。

调用图:调用 1 个内部函数(command_input_json);外部调用 3 个(assert_eq!, request_for_tool_use, from_str)。

tests::block_decision_stops_normal_processing344–364 ↗
fn block_decision_stops_normal_processing()

作用:测试当钩子输出“block”决定时,系统会把它当成拦截,并把原因反馈给模型。

数据流:它构造一个退出码为 0、stdout 里写着 decision=block 和 reason 的命令结果 → 交给 parse_completed → 检查解析结果里的 should_block 为 true,状态是 Blocked,反馈消息包含钩子给出的原因。

调用关系:这个测试盯住 parse_completed 的拦截分支。它用 handler 和 run_result 造出最小场景,确保正常成功退出的脚本也可以通过 JSON 明确要求拦截。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::additional_context_is_recorded367–393 ↗
fn additional_context_is_recorded()

作用:测试钩子可以给模型补充额外上下文,而且这段内容会同时进入运行记录和返回数据。

数据流:它构造一个 stdout 含 additionalContext 的成功命令结果 → 调用 parse_completed → 检查 data 里保存了这段上下文,completed.run.entries 里也有一条 Context 类型记录。

调用关系:这个测试验证 parse_completed 和 common::append_additional_context 配合正确。它确保钩子不是只能拦截,也能温和地给模型追加提示。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::unsupported_updated_mcp_tool_output_fails_open396–423 ↗
fn unsupported_updated_mcp_tool_output_fails_open()

作用:测试 PostToolUse 钩子如果返回当前不支持的 updatedMCPToolOutput,不会真的改工具输出,而是记为失败但不拦截。这里的“fails open”意思是失败时不把路堵死。

数据流:它构造一个 stdout,里面包含 unsupported 的 updatedMCPToolOutput → 交给 parse_completed → 检查 should_block 仍是 false,但运行状态是 Failed,并记录明确的错误文字。

调用关系:这个测试保护 parse_completed 对不支持字段的处理。它避免外部脚本以为自己能修改 MCP 工具结果,从而造成系统行为不一致。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::exit_two_blocks_with_feedback426–442 ↗
fn exit_two_blocks_with_feedback()

作用:测试钩子进程用退出码 2 表示“拦截”,并且 stderr 里的文字会成为反馈。退出码就是程序结束时给系统的数字信号。

数据流:它构造一个 exit_code=2、stderr 写着反馈文字的命令结果 → 调用 parse_completed → 检查 should_block 为 true,状态是 Blocked,反馈消息就是 stderr 的内容。

调用关系:这个测试覆盖 parse_completed 的兼容规则:除了 JSON 输出,钩子也可以用退出码 2 加错误输出这种简单方式来阻止继续。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::continue_false_stops_with_reason445–472 ↗
fn continue_false_stops_with_reason()

作用:测试钩子输出 continue=false 时,系统会停止后续处理,并优先把 reason 作为给模型的反馈。

数据流:它构造一个成功命令结果,stdout 里包含 continue=false、stopReason 和 reason → 调用 parse_completed → 检查状态是 Stopped,记录里的停止文字来自 stopReason,给模型的反馈来自 reason。

调用关系:这个测试保护 parse_completed 对“停止”和“拦截”的区分。停止不等于 should_block,但仍会告诉模型为什么不要继续。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::continue_false_without_reason_synthesizes_feedback475–498 ↗
fn continue_false_without_reason_synthesizes_feedback()

作用:测试当钩子只说 continue=false、却没给原因时,系统会自己补一句默认反馈,避免模型收到空消息。

数据流:它构造一个 stdout 只有 continue=false 的成功命令结果 → 调用 parse_completed → 检查状态是 Stopped,并且反馈和记录都使用默认文字“PostToolUse hook stopped execution”。

调用关系:这个测试覆盖 parse_completed 的兜底行为。它保证外部脚本输出不完整时,用户和模型仍能看到一个可理解的停止原因。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::plain_stdout_is_ignored_for_post_tool_use501–518 ↗
fn plain_stdout_is_ignored_for_post_tool_use()

作用:测试普通文本 stdout 不会被误当成钩子指令。也就是说,脚本随便打印一句话不会突然拦截或报错。

数据流:它构造一个退出码为 0、stdout 只是普通文字的命令结果 → 调用 parse_completed → 检查状态仍是 Completed,既没有额外数据,也没有输出条目。

调用关系:这个测试保护 parse_completed 的宽容规则:只有看起来像受支持 JSON 的输出才会被解释为指令;普通日志文本会被忽略。

调用图:调用 1 个内部函数(parse_completed);外部调用 3 个(assert_eq!, handler, run_result)。

tests::preview_and_completed_run_ids_include_tool_use_id521–542 ↗
fn preview_and_completed_run_ids_include_tool_use_id()

作用:测试预览阶段和完成阶段生成的运行编号都包含 tool_use_id。这样同一个工具调用的钩子记录不会和别的调用混在一起。

数据流:它先用 request_for_tool_use 造一个带 tool_use_id 的请求 → 调用 preview 生成预览摘要并检查 id → 再用 parse_completed 造完成事件,并通过 common::hook_completed_for_tool_use 补上工具调用 id → 最后确认完成 id 和预览 id 对齐。

调用关系:这个测试把 preview、parse_completed 和 common::hook_completed_for_tool_use 串起来检查。它保证上层界面或日志系统可以用同一个 id 跟踪钩子的完整生命周期。

调用图:调用 3 个内部函数(hook_completed_for_tool_use, parse_completed, preview);外部调用 4 个(assert_eq!, handler, request_for_tool_use, run_result)。

tests::serialization_failure_run_ids_include_tool_use_id545–558 ↗
fn serialization_failure_run_ids_include_tool_use_id()

作用:测试即使输入 JSON 序列化失败,生成的失败事件编号也要包含 tool_use_id。失败记录也必须能追溯到具体那次工具调用。

数据流:它造一个带 tool_use_id 的请求 → 先调用 preview 得到预期运行 id → 再调用 common::serialization_failure_hook_events_for_tool_use 生成失败事件 → 检查失败事件里的 id 和预览 id 一致。

调用关系:这个测试保护 run 的错误路径所依赖的编号规则。它不直接调用 run,而是检查 serialization_failure_hook_events_for_tool_use 与 preview 的编号约定一致。

调用图:调用 2 个内部函数(serialization_failure_hook_events_for_tool_use, preview);外部调用 4 个(assert_eq!, handler, request_for_tool_use, vec!)。

tests::handler560–572 ↗
fn handler() -> ConfiguredHandler

作用:为测试制造一个假的 PostToolUse 处理器配置。它相当于测试里的“样板钩子脚本”。

数据流:它不接收输入 → 构造一个匹配 Bash 的 ConfiguredHandler,命令是 python3 post_tool_use_hook.py,带超时时间、状态消息、来源路径和显示顺序 → 输出这个处理器给其他测试使用。

调用关系:许多测试会调用它来避免重复写配置。parse_completed 和 preview 的测试都需要一个处理器,因为完成摘要和运行编号都要从处理器配置里取信息。

调用图:外部调用 2 个(test_path_buf, new)。

tests::run_result574–584 ↗
fn run_result(exit_code: Option<i32>, stdout: &str, stderr: &str) -> CommandRunResult

作用:为测试制造一个假的命令运行结果。测试可以用它快速模拟脚本成功、失败、拦截或输出不同内容的情况。

数据流:输入是退出码、stdout 字符串和 stderr 字符串 → 它填入固定的开始时间、结束时间、耗时,并把错误字段设为空 → 输出 CommandRunResult。

调用关系:几乎所有 parse_completed 的测试都会用它。它让测试重点放在“怎么解释结果”,而不是每次都手写完整的命令结果结构。

tests::request_for_tool_use586–601 ↗
fn request_for_tool_use(tool_use_id: &str) -> super::PostToolUseRequest

作用:为测试制造一份假的工具调用请求。它代表一次 Bash 工具调用已经完成,等待 PostToolUse 钩子检查。

数据流:输入是 tool_use_id → 它生成新的 session_id,填入 turn_id、当前目录、模型名、权限模式、工具名 Bash、工具输入和工具响应 → 输出 PostToolUseRequest。

调用关系:preview 和序列化相关测试会调用它。它为 command_input_json 和 preview 提供稳定的测试数据,尤其方便检查 tool_use_id 是否被正确带进运行编号。

调用图:调用 1 个内部函数(new);外部调用 3 个(new, test_path_buf, json!)。

hooks/src/events/permission_request.rs源码 ↗
orchestrationrequest handling

这段代码像审批窗口前的一道安检。工具想执行前,系统会先找出匹配这个工具名的钩子处理器(钩子就是预先配置的小程序或命令,用来插入额外规则)。它先提供 preview,让界面能显示“哪些检查正在等结果”;真正运行时,它把当前会话、目录、模型、权限模式、工具输入等信息打包成 JSON 交给这些处理器。每个处理器跑完后,parse_completed 会把输出翻译成人能看到的提示、错误或拒绝理由。最后它很保守地合并结论:只要有一个拒绝,就拒绝;如果没人拒绝,最后一个允许才算允许;都没表态,就交回正常审批流程。

函数细节8
preview67–85 ↗
fn preview(
    handlers: &[ConfiguredHandler],
    request: &PermissionRequestRequest,
) -> Vec<HookRunSummary>

作用:提前算出这次权限请求会触发哪些钩子,但不真正运行它们。这样界面可以先显示“这些检查将要执行”,让用户看到等待中的钩子行。

数据流:输入是一组已配置的处理器和这次权限请求的信息。它先根据工具名和别名做匹配,挑出适用的处理器,再把每个处理器包装成一条运行摘要;输出是一组 HookRunSummary,用来给界面展示。

调用关系:它由 preview_permission_request 调用,属于真正执行前的预告步骤。它把匹配工作交给 matcher_inputs 和 select_handlers_for_matcher_inputs,自己只负责把选中的处理器变成适合工具使用场景的预览记录。

调用图:调用 2 个内部函数(select_handlers_for_matcher_inputs, matcher_inputs);被 1 处调用(preview_permission_request)。

run87–148 ↗
async fn run(
    handlers: &[ConfiguredHandler],
    shell: &CommandShell,
    request: PermissionRequestRequest,
) -> PermissionRequestOutcome

作用:真正执行权限请求钩子,并给出最后的钩子裁决。它决定这次工具请求是被钩子允许、被钩子拒绝,还是钩子不做决定。

数据流:输入是处理器列表、命令运行环境 shell,以及完整的权限请求。它先挑出匹配的处理器;如果没有,就返回空事件和无决定。若有匹配,它把请求打包成 JSON,交给处理器逐个执行;随后收集每个处理器的完成事件,并从解析出的决定里合并出最终结果。输出是 PermissionRequestOutcome,里面有给记录和界面看的钩子事件,以及可选的允许或拒绝决定。

调用关系:它由 run_permission_request 调用,是这个文件的主流程。它依次使用 matcher_inputs、select_handlers_for_matcher_inputs、build_command_input、execute_handlers 和 resolve_permission_request_decision;如果输入序列化失败,就走 serialization_failure_hook_events_for_tool_use 生成失败事件。

调用图:调用 6 个内部函数(execute_handlers, select_handlers_for_matcher_inputs, matcher_inputs, serialization_failure_hook_events_for_tool_use, build_command_input, resolve_permission_request_decision);被 1 处调用(run_permission_request);外部调用 3 个(new, format!, to_string)。

resolve_permission_request_decision153–170 ↗
fn resolve_permission_request_decision(
    decisions: impl IntoIterator<Item = &'a PermissionRequestDecision>,
) -> Option<PermissionRequestDecision>

作用:把多个钩子的表态合成一个最终表态。规则很保守:任何一个拒绝都算拒绝;没有拒绝时,允许才会生效;没人表态就没有结论。

数据流:输入是一串钩子决定。它从前到后看:遇到允许就先记下来,遇到拒绝就立刻复制拒绝理由并返回;如果看完只有允许,就返回允许;如果什么决定都没有,就返回 None。

调用关系:它由 run 在所有处理器执行完后调用。run 负责收集原始决定,这个函数负责把这些决定按安全优先的规则折叠成一个结果。

调用图:被 1 处调用(run)。

build_command_input172–187 ↗
fn build_command_input(request: &PermissionRequestRequest) -> PermissionRequestCommandInput

作用:把系统内部的权限请求,整理成能发给外部钩子命令的标准输入数据。外部命令不需要懂内部结构,只看这份统一格式就行。

数据流:输入是 PermissionRequestRequest。它取出会话编号、轮次编号、子代理信息、当前目录、对话记录路径、模型、权限模式、工具名和工具输入,并把路径等内容转换成适合 JSON 的字段;输出是 PermissionRequestCommandInput。

调用关系:它由 run 在执行处理器前调用。run 随后会把这个结果序列化成 JSON,作为 execute_handlers 运行外部处理器时的输入。

调用图:调用 2 个内部函数(from_path, from);被 1 处调用(run)。

parse_completed189–291 ↗
fn parse_completed(
    handler: &ConfiguredHandler,
    run_result: CommandRunResult,
    turn_id: Option<String>,
) -> dispatcher::ParsedHandler<PermissionRequestHandlerData>

作用:把一个钩子命令跑完后的原始结果,翻译成系统能理解、用户也能看到的完成事件和可选决定。它负责判断成功、失败、拒绝,以及整理提示文字。

数据流:输入是处理器信息、命令运行结果和可选的轮次编号。它检查有没有运行错误、退出码是多少、标准输出是不是合法的权限请求 JSON、标准错误里有没有拒绝理由;然后生成显示用的输出条目,设置完成、失败或阻止状态,并提取允许或拒绝决定。输出是 ParsedHandler,里面既有 HookCompletedEvent,也有本文件后续合并要用的 decision。

调用关系:它作为回调交给 execute_handlers 使用:每个处理器命令结束后,调度器会用它解析结果。它会调用 parse_permission_request、looks_like_json、trimmed_non_empty 和 completed_summary,把低层命令结果变成权限请求流程需要的结构。

调用图:调用 4 个内部函数(completed_summary, looks_like_json, parse_permission_request, trimmed_non_empty);外部调用 2 个(new, format!)。

tests::permission_request_deny_overrides_earlier_allow301–315 ↗
fn permission_request_deny_overrides_earlier_allow()

作用:验证安全规则里“拒绝优先”真的生效。即使前面有钩子说允许,后面只要有拒绝,最终也必须拒绝。

数据流:输入是一组测试用决定:先允许,再拒绝并带有理由。测试把它们交给 resolve_permission_request_decision,然后断言输出是带同样理由的拒绝。

调用关系:这是 resolve_permission_request_decision 的单元测试。它不参与正常运行,只在测试时用 assert_eq! 检查合并规则不会被改坏。

调用图:外部调用 1 个(assert_eq!)。

tests::permission_request_returns_allow_when_no_handler_denies318–328 ↗
fn permission_request_returns_allow_when_no_handler_denies()

作用:验证在没有任何钩子拒绝时,允许决定会被保留下来。这样明确放行的钩子不会被误当成“没意见”。

数据流:输入是两个允许决定。测试把它们交给 resolve_permission_request_decision,然后断言输出是 Allow。

调用关系:这是合并规则的另一条单元测试。它专门覆盖“全是允许、没有拒绝”的情况,确保 run 后续拿到的最终决定符合预期。

调用图:外部调用 1 个(assert_eq!)。

tests::permission_request_returns_none_when_no_handler_decides331–335 ↗
fn permission_request_returns_none_when_no_handler_decides()

作用:验证没有钩子表态时,系统不会凭空造出允许或拒绝。这样正常的用户审批流程还能继续接手。

数据流:输入是一个空的决定列表。测试调用 resolve_permission_request_decision,断言输出是 None,表示没有钩子裁决。

调用关系:这是合并规则的边界测试。它确保 run 在所有处理器都不做决定,或没有可用决定时,会把流程交回默认审批路径。

调用图:外部调用 2 个(new, assert_eq!)。