Gemini CLI Hooks 深入解析:11 个生命周期钩子、stdin/stdout JSON 协议与双层安全模型
本文基于 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_name、tool_input,MCP 工具还附带mcp_context(服务器名、连接方式等非敏感身份信息);AfterTool额外包含tool_response; - Agent 事件:
BeforeAgent输入包含用户prompt;AfterAgent包含prompt、prompt_response和stop_hook_active标志(防止钩子自身触发无限重试); - 会话事件:
SessionStart输入带source(startup/resume/clear,见 types.ts);SessionEnd输入带reason(exit/clear/logout/prompt_input_exit/other); - 模型事件:
BeforeModel/AfterModel/BeforeToolSelection使用解耦的llm_request(及响应)结构,避免把 SDK 内部对象直接暴露给钩子; - 压缩与通知:
PreCompress输入带trigger(manual/auto);Notification输入带notification_type、message与details(目前通知类型为工具权限确认ToolPermission)。
事件输出:每个钩子“能做什么”
钩子输出的基础字段(HookOutput)包括 continue、stopReason、suppressOutput、systemMessage、decision、reason 与事件专属的 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 / allow(types.ts)。block 与 deny 在语义上都是阻断决策(isBlockingDecision()),ask 表示请求用户确认,continue: false 则直接停止执行(对应 stopReason 作为停止原因)。
三、全局机制:stdin/stdout 协议与退出码
严格 JSON 要求(“黄金法则”)
Hooks 通过 stdin(输入)与 stdout(输出)与 CLI 通信,必须遵守三条规则:
- 静默是强制的:脚本不得向
stdout输出最终 JSON 对象以外的任何纯文本。哪怕在 JSON 之前多一个echo或print,都会破坏解析。 - 污染即失败:若
stdout含非 JSON 文本,解析失败时 CLI 默认按“允许”处理,并把整段输出当作systemMessage。 - 调试走 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.ts 的 convertPlainTextToHookOutput 可以确认降级路径的精确行为:退出码 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_request、BeforeTool合并tool_input)。
四、Matchers:精确控制钩子的触发范围
用 matcher 字段过滤哪些工具或触发器会命中你的钩子:
- 工具事件(
BeforeTool、AfterTool):matcher 是正则表达式(例如"write_.*"); - 生命周期事件:matcher 是精确字符串(例如
"startup"); - 通配:
"*"或""(空字符串)匹配所有。
源码实现(hookPlanner.ts)印证了这套规则并补充了两点:
- 容错回退:工具名匹配时先尝试
new RegExp(matcher),若正则非法则退化为字面量精确比较; - 计划去重与串行开关:
HookPlanner.createExecutionPlan会按name:command组合键(getHookKey)对相同钩子去重;只要某条钩子定义声明了sequential: true,该事件下的全部钩子都会改为串行执行——这是让多个钩子形成“处理链”的官方手段。
五、配置:四层来源与合并优先级
Hooks 配置写在 settings.json 中,Gemini CLI 按以下优先级从高到低合并多个来源:
- 项目设置:当前目录的
.gemini/settings.json; - 用户设置:
~/.gemini/settings.json; - 系统设置:
/etc/gemini-cli/settings.json; - 扩展:已安装扩展定义的钩子。
从 hookRegistry.ts 的 getSourcePriority 看,源码中还存在一个优先级更高的 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 字段显式传入。
七、多钩子决策如何合并
当同一事件命中多个钩子时,HookAggregator(hookAggregator.ts)按事件类型选择合并策略,这是理解“多个安全钩子叠加”行为的关键:
- OR 决策逻辑(
BeforeTool、AfterTool、BeforeAgent、AfterAgent、SessionStart):只要任一钩子给出阻断决策(block/deny),最终即为阻断;ask仅在无阻断时生效;若无任何阻断/询问/continue: false,最终决策默认allow。多个钩子的reason、systemMessage、additionalContext会按换行拼接,suppressOutput采用“任一为真即真”。 - 字段替换(
BeforeModel、AfterModel):后执行的输出覆盖先前的输出,适合“后一个钩子对前一个的修改做再加工”。 - 工具选择合并(
BeforeToolSelection):采取并集策略——任一钩子声明NONE模式则整体最严格(无工具可用);否则任一声明ANY则用ANY;默认AUTO。允许的工具名取所有钩子的并集并排序,保证缓存一致性。 - 简单合并(其余事件,如
PreCompress、Notification、SessionEnd):字段直接叠加。
八、安全与风险
警告:Hooks 以你的用户权限执行任意代码。配置钩子即允许脚本在你的机器上运行 shell 命令。
项目级钩子在打开不受信任的项目时风险尤其突出。Gemini CLI 对此建立了两道防线,均可在源码中确认:
- 指纹信任机制(trustedHooks.ts):CLI 会为项目钩子建立指纹——以
name:command组合键(getHookKey)存入全局trusted_hooks.json。当钩子的名称或命令发生变化(例如通过git pull引入修改)时,它被视为新的、不受信任的钩子,CLI 会发出警告;用户确认知情后该指纹被写入信任列表,避免重复打扰。 - 信任目录门禁(hookRunner.ts 与 hookRegistry.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/ 源码为权威依据深入排查任何钩子行为问题。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00