首页
/ Gemini CLI Hooks 深入解析:11 个生命周期钩子、stdin/stdout JSON 协议与双层安全模型

Gemini CLI Hooks 深入解析:11 个生命周期钩子、stdin/stdout JSON 协议与双层安全模型

2026-09-04 09:54:10作者:田桥桑Industrious

本文基于 Gemini CLI 的 Hooks 文档(docs/hooks/index.md)并结合 packages/core/src/hooks/ 下的核心实现展开,系统讲解如何在 Agent 循环的 11 个生命周期事件中注入外部脚本、通过 stdin/stdout JSON 协议与退出码控制工具调用和模型请求、按四层配置体系装配钩子,以及如何理解项目级钩子的指纹信任机制。读完本文,你可以独立完成一个可用的钩子脚本配置,并能从源码层面解释“污染 stdout 为什么会退化为允许”“多个钩子的决策如何合并”等关键行为。

一、Hooks 是什么

Hooks 是 Gemini CLI 在 Agent 循环(agentic loop)特定点位执行的脚本或程序,允许你在不修改 CLI 源码的前提下拦截并定制其行为。

从执行语义看,Hooks 是同步执行的:当某个钩子事件触发时,Gemini CLI 会等待所有匹配的钩子完成后再继续推进 Agent 循环。这一点在实现上对应 HookRunner 对子进程的生命周期管理(hookRunner.ts 中通过 spawn 启动子进程、写入 stdin、收集 stdout/stderr 并在超时后强制终止)。

借助 Hooks,你可以做到:

  • 注入上下文:在模型处理请求前注入相关信息(例如 git 历史);
  • 校验动作:审查工具参数,拦截潜在危险操作;
  • 强制策略:实现安全扫描器与合规检查;
  • 记录交互:跟踪工具使用与模型响应,用于审计;
  • 优化行为:动态过滤可用工具或调整模型参数。

配套的三篇文档构成完整的知识体系,建议按顺序阅读:

二、钩子事件全览

Hooks 由 Gemini CLI 生命周期中的特定事件触发。文档定义的 11 个事件与源码中 HookEventName 枚举(types.ts)一一对应:

事件 触发时机 影响能力 典型用例
SessionStart 会话开始时(startup、resume、clear) 注入上下文 初始化资源、加载上下文
SessionEnd 会话结束时(exit、clear) 建议性(Advisory) 清理、保存状态
BeforeAgent 用户提交 prompt 后、规划前 阻断轮次 / 注入上下文 补充上下文、校验 prompt、拦截轮次
AfterAgent Agent 循环结束时 重试 / 中止 审查输出、强制重试或中止执行
BeforeModel 向 LLM 发送请求前 阻断轮次 / Mock 修改 prompt、切换模型、伪造响应
AfterModel 收到 LLM 响应后 阻断轮次 / 脱敏 过滤/脱敏响应、记录交互
BeforeToolSelection LLM 选择工具前 过滤工具 过滤可用工具、优化选择
BeforeTool 工具执行前 阻断工具 / 重写参数 校验参数、拦截危险操作
AfterTool 工具执行后 阻断结果 / 注入上下文 处理结果、跑测试、隐藏结果
PreCompress 上下文压缩前 建议性(Advisory) 保存状态、通知用户
Notification 系统通知发生时 建议性(Advisory) 转发桌面提醒、记录日志

事件输入:每个钩子“看到”什么

所有事件的输入共享一个基础结构(types.ts 中的 HookInput):

字段 含义
session_id 当前会话唯一 ID
transcript_path 会话记录路径
cwd 当前工作目录
hook_event_name 当前触发的事件名
timestamp 时间戳

在此之上,各事件携带特有字段,决定了钩子的能力边界:

  • 工具事件BeforeTool / AfterTool 输入包含 tool_nametool_input,MCP 工具还附带 mcp_context(服务器名、连接方式等非敏感身份信息);AfterTool 额外包含 tool_response
  • Agent 事件BeforeAgent 输入包含用户 promptAfterAgent 包含 promptprompt_responsestop_hook_active 标志(防止钩子自身触发无限重试);
  • 会话事件SessionStart 输入带 sourcestartup / resume / clear,见 types.ts);SessionEnd 输入带 reasonexit / clear / logout / prompt_input_exit / other);
  • 模型事件BeforeModel / AfterModel / BeforeToolSelection 使用解耦的 llm_request(及响应)结构,避免把 SDK 内部对象直接暴露给钩子;
  • 压缩与通知PreCompress 输入带 triggermanual / auto);Notification 输入带 notification_typemessagedetails(目前通知类型为工具权限确认 ToolPermission)。

事件输出:每个钩子“能做什么”

钩子输出的基础字段(HookOutput)包括 continuestopReasonsuppressOutputsystemMessagedecisionreason 与事件专属的 hookSpecificOutput。不同事件在 hookSpecificOutput 中的扩展能力差异很大(见 types.ts):

事件 专属输出能力
BeforeTool tool_input:重写工具入参(与原始参数合并)
AfterTool additionalContext:向模型追加上下文;tailToolCallRequest:请求紧接执行另一个工具,其结果将替换原工具响应
BeforeModel llm_request:修改请求(模型、配置、内容);llm_response:提供合成响应以跳过真实模型调用
AfterModel llm_response:替换/改写模型响应(可用于脱敏)
BeforeToolSelection toolConfig:控制工具调用模式与允许的工具名列表
AfterAgent clearContext:请求清空上下文
SessionStart / BeforeAgent additionalContext:注入上下文(会做 < > 转义防标签注入)

decision 字段的合法取值为 ask / block / deny / approve / allowtypes.ts)。blockdeny 在语义上都是阻断决策(isBlockingDecision()),ask 表示请求用户确认,continue: false 则直接停止执行(对应 stopReason 作为停止原因)。

三、全局机制:stdin/stdout 协议与退出码

严格 JSON 要求(“黄金法则”)

Hooks 通过 stdin(输入)与 stdout(输出)与 CLI 通信,必须遵守三条规则:

  1. 静默是强制的:脚本不得stdout 输出最终 JSON 对象以外的任何纯文本。哪怕在 JSON 之前多一个 echoprint,都会破坏解析。
  2. 污染即失败:若 stdout 含非 JSON 文本,解析失败时 CLI 默认按“允许”处理,并把整段输出当作 systemMessage
  3. 调试走 stderr:所有日志与调试信息应输出到 stderr(如 echo "debug" >&2)。Gemini CLI 会捕获 stderr从不将其当作 JSON 解析。

这条“污染即失败、退化为允许”的行为在源码中可以直接验证(hookRunner.ts):进程结束后,CLI 先取 stdout.trim() || stderr.trim() 尝试 JSON.parse(支持“JSON 字符串再套一层”的情况);解析失败时调用 convertPlainTextToHookOutput,把纯文本降级为结构化输出。

退出码

Gemini CLI 用退出码决定钩子执行的高层结果:

退出码 标签 行为影响
0 Success stdout 被解析为 JSON。这是首选退出码,适用于一切逻辑,包括有意的阻断(例如输出 {"decision": "deny"}
2 System Block 严重阻断。目标动作(工具、轮次或停止)被中止,stderr 内容作为拒绝原因。高严重级别,用于安全停机或脚本失败
其他 Warning 非致命失败。显示一条警告,但交互使用原始参数继续

hookRunner.tsconvertPlainTextToHookOutput 可以确认降级路径的精确行为:退出码 0 时纯文本输出被转换为 { decision: "allow", systemMessage: text };退出码 1 被专门定义为非阻断错误(EXIT_CODE_NON_BLOCKING_ERROR),转换为带 Warning: 前缀的 systemMessage;而退出码 2 及其他非零码则转换为 { decision: "deny", reason: text }。也就是说,“用 JSON 表达决策、用退出码表达严重性” 是两套互补的通道:精细控制请返回退出码 0 + JSON,粗粒度安全停机则直接用退出码 2。

进程执行的工程细节

阅读 hookRunner.ts 可以看到几个保证钩子健壮性的实现点:

  • 超时强制:每个钩子有 timeout(毫秒,默认 60000),超时先 SIGTERM(Windows 用 taskkill),5 秒后仍未退出则 SIGKILL
  • Shell 适配:通过 getShellConfiguration() 选择执行 shell,PowerShell 下会追加 $LASTEXITCODE 检查以正确传播退出码;
  • 变量展开:命令字符串中的 $GEMINI_PROJECT_DIR$GEMINI_CWD$GEMINI_PLANS_DIR$GEMINI_SESSION_ID$CLAUDE_PROJECT_DIR 会在执行前被替换为转义后的实际值(expandCommand);
  • 并行与串行:默认同一事件的多个钩子并行执行(executeHooksParallel);串行模式下前一个钩子的输出会折叠进下一个钩子的输入(如 BeforeAgent 追加上下文、BeforeModel 合并 llm_requestBeforeTool 合并 tool_input)。

四、Matchers:精确控制钩子的触发范围

matcher 字段过滤哪些工具或触发器会命中你的钩子:

  • 工具事件BeforeToolAfterTool):matcher 是正则表达式(例如 "write_.*");
  • 生命周期事件:matcher 是精确字符串(例如 "startup");
  • 通配"*"""(空字符串)匹配所有。

源码实现(hookPlanner.ts)印证了这套规则并补充了两点:

  1. 容错回退:工具名匹配时先尝试 new RegExp(matcher),若正则非法则退化为字面量精确比较;
  2. 计划去重与串行开关HookPlanner.createExecutionPlan 会按 name:command 组合键(getHookKey)对相同钩子去重;只要某条钩子定义声明了 sequential: true,该事件下的全部钩子都会改为串行执行——这是让多个钩子形成“处理链”的官方手段。

五、配置:四层来源与合并优先级

Hooks 配置写在 settings.json 中,Gemini CLI 按以下优先级从高到低合并多个来源:

  1. 项目设置:当前目录的 .gemini/settings.json
  2. 用户设置~/.gemini/settings.json
  3. 系统设置/etc/gemini-cli/settings.json
  4. 扩展:已安装扩展定义的钩子。

hookRegistry.tsgetSourcePriority 看,源码中还存在一个优先级更高的 Runtime 来源(程序注册钩子,如扩展/插件在运行时注册),排序为 Runtime → Project → User → System → Extensions。注册表还会对配置做校验:事件名必须是 11 个合法事件之一;type 必须为 command(及运行时类型);type: "command" 时必须有 command 字段,否则整条配置被丢弃并记入调试日志。

配置模式(Schema)

{
  "hooks": {
    "BeforeTool": [
      {
        "matcher": "write_file|replace",
        "hooks": [
          {
            "name": "security-check",
            "type": "command",
            "command": "$GEMINI_PROJECT_DIR/.gemini/hooks/security.sh",
            "timeout": 5000
          }
        ]
      }
    ]
  }
}

钩子配置字段

字段 类型 必填 说明
type string 执行引擎,目前配置层面仅支持 "command"
command string 是* 要执行的 shell 命令(type"command" 时必填)
name string 友好名称,用于在日志和 CLI 命令中识别钩子
timeout number 执行超时(毫秒),默认 60000
description string 简要说明钩子用途

补充一个源码中的细节:CommandHookConfig 还支持可选的 env 字段(types.ts),用于为单个钩子注入额外环境变量,且会合并到钩子执行环境中。

六、环境变量:钩子的“身份”

钩子在净化的环境(sanitized environment)中执行,通过 sanitizeEnvironment 过滤宿主环境后,CLI 显式注入以下变量(hookRunner.ts):

变量 含义
GEMINI_PROJECT_DIR 项目根的绝对路径
GEMINI_PLANS_DIR plans 目录的绝对路径
GEMINI_SESSION_ID 当前会话唯一 ID
GEMINI_CWD 当前工作目录
CLAUDE_PROJECT_DIR (兼容别名)为兼容性提供,值与 GEMINI_PROJECT_DIR 相同

这意味着钩子脚本可以直接引用 $GEMINI_PROJECT_DIR/.gemini/hooks/... 这类路径而无需硬编码;同时也意味着钩子默认拿不到宿主的敏感环境变量——如确实需要某个变量,应通过钩子配置的 env 字段显式传入。

七、多钩子决策如何合并

当同一事件命中多个钩子时,HookAggregatorhookAggregator.ts)按事件类型选择合并策略,这是理解“多个安全钩子叠加”行为的关键:

  • OR 决策逻辑BeforeToolAfterToolBeforeAgentAfterAgentSessionStart):只要任一钩子给出阻断决策(block/deny),最终即为阻断;ask 仅在无阻断时生效;若无任何阻断/询问/continue: false,最终决策默认 allow。多个钩子的 reasonsystemMessageadditionalContext 会按换行拼接,suppressOutput 采用“任一为真即真”。
  • 字段替换BeforeModelAfterModel):后执行的输出覆盖先前的输出,适合“后一个钩子对前一个的修改做再加工”。
  • 工具选择合并BeforeToolSelection):采取并集策略——任一钩子声明 NONE 模式则整体最严格(无工具可用);否则任一声明 ANY 则用 ANY;默认 AUTO。允许的工具名取所有钩子的并集并排序,保证缓存一致性。
  • 简单合并(其余事件,如 PreCompressNotificationSessionEnd):字段直接叠加。

八、安全与风险

警告:Hooks 以你的用户权限执行任意代码。配置钩子即允许脚本在你的机器上运行 shell 命令。

项目级钩子在打开不受信任的项目时风险尤其突出。Gemini CLI 对此建立了两道防线,均可在源码中确认:

  1. 指纹信任机制trustedHooks.ts):CLI 会为项目钩子建立指纹——以 name:command 组合键(getHookKey)存入全局 trusted_hooks.json。当钩子的名称或命令发生变化(例如通过 git pull 引入修改)时,它被视为新的、不受信任的钩子,CLI 会发出警告;用户确认知情后该指纹被写入信任列表,避免重复打扰。
  2. 信任目录门禁hookRunner.tshookRegistry.ts):项目钩子只在受信任目录(trusted folder)中加载与执行;在非受信目录中,项目级钩子会被直接拦截(“Security: Blocked execution of project hook in untrusted folder”),注册表层面也会整体跳过项目钩子处理。

更完整的威胁模型与“安全使用钩子”的准则,见 最佳实践文档

九、管理钩子:/hooks 命令

无需手改 JSON,CLI 内置 /hooks 命令族(实现见 hooksCommand.ts):

命令 作用
/hooks panel 查看所有已注册钩子及其状态
/hooks enable-all 启用全部钩子
/hooks disable-all 禁用全部钩子
/hooks enable <name> 启用指定名称的钩子
/hooks disable <name> 禁用指定名称的钩子

禁用状态通过注册表的 enabled 标志与设置中的禁用列表生效(HookRegistry.setHookEnabled 按名称匹配并更新)。对于需要回归验证钩子行为的场景,仓库提供了大规模集成测试:hooks-system.test.ts 覆盖了工具阻断(block-tool)、上下文注入(after-tool-context)、输入改写(input-modification)、顺序执行(sequential-execution)、会话启动/清空(session-startup / session-clear)等场景,每个场景配有对应的 .responses 回放文件,可作为钩子行为的“活规范”参考。

小结

Gemini CLI 的 Hooks 机制把“拦截点 + I/O 协议 + 退出码 + 配置分层 + 安全门禁”组装成一套完整的定制体系:11 个事件覆盖了从会话开始到上下文压缩的完整 Agent 生命周期;stdin/stdout 的严格 JSON 契约与退出码语义让钩子既能精细改写请求/响应,也能一键安全停机;项目级指纹信任与受信目录门禁则把“执行任意代码”的风险约束在用户知情同意的边界内。掌握本文内容后,你可以从 写作指南 起步编写第一个钩子,并以 技术参考packages/core/src/hooks/ 源码为权威依据深入排查任何钩子行为问题。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
docsdocs
暂无描述
Markdown
889
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341