Hooks:生命周期钩子


文档摘要

Hooks:生命周期钩子 本节摘要:Hooks(钩子)是 Grok Build 一个独特的机制——它让你在 Agent 的生命周期事件上挂钩,执行自定义逻辑。最典型的:在工具执行前(PreToolUse),你的 hook 可以决定要不要放行,实现自定义安全策略。Hook 可以是本地命令(command),也可以是 HTTP 回调(http),这让 hooks 既能做轻量本地检查,也能做集成外部系统的复杂逻辑。本节会讲清 hooks 的核心生命周期事件、两种 handler 类型、退出码约定与 fail-open 语义、发现位置,以及它作为「用户级安全策略」的角色。

Hooks:生命周期钩子

本节摘要:Hooks(钩子)是 Grok Build 一个独特的机制——它让你在 Agent 的生命周期事件上挂钩,执行自定义逻辑。最典型的:在工具执行前(PreToolUse),你的 hook 可以决定要不要放行,实现自定义安全策略。Hook 可以是本地命令(command),也可以是 HTTP 回调(http),这让 hooks 既能做轻量本地检查,也能做集成外部系统的复杂逻辑。本节会讲清 hooks 的核心生命周期事件、两种 handler 类型、退出码约定与 fail-open 语义、发现位置,以及它作为「用户级安全策略」的角色。

一、Hooks 在系统中的双重角色

Hooks 在 Grok Build 里扮演双重角色:

角色一:扩展机制

Hooks 让你「在事件发生时做点什么」。比如:

  • 会话开始时(SessionStart),初始化某些环境
  • 工具执行后(PostToolUse),记录日志或通知
  • 会话结束(SessionEnd),清理资源

这种「事件驱动」的扩展,让外部系统能与 Grok Build 的生命周期深度集成。

角色二:安全机制

最关键的事件是 PreToolUse——它在工具执行触发,hook 可以拒绝这次执行。这让你能实现自定义安全策略,比如:

  • 禁止在工作目录外写文件」(检查路径)
  • 只允许特定 git 命令」(检查命令)
  • 敏感操作要二次确认」(弹外部审批)

回顾第 5 章的工具调用生命周期,PreToolUse hooks 是鉴权管线的第一道闸门。它是用户自定义安全策略的落点。

二、核心生命周期事件

Grok Build 的 hooks 系统定义了多个生命周期事件(在 xai-grok-hooks/src/event.rs)。完整的事件清单较长,v0 版本主要启用四个核心:

HookEventName: ├── SessionStart # 会话开始 ├── PreToolUse ★ # 工具执行前(可拒绝) ├── PostToolUse # 工具执行后 ├── PostToolUseFailure # 工具执行失败后 ├── PermissionDenied # 权限被拒绝时 ├── UserPromptSubmit # 用户提交 prompt 时 ├── Notification # 通知事件 ├── SubagentStart / SubagentStop # 子代理开始/停止 ├── PreCompact / PostCompact # 压缩前/后(第 7 章) ├── Stop / StopFailure # 会话停止 └── SessionEnd # 会话结束

v0 启用的四个:SessionStart、PreToolUse、PostToolUse、SessionEnd。其他事件可能在后续版本启用,或作为预留。

事件的分类

这些事件大致分两类:

  • 通知型(PostToolUse、SessionEnd 等):只是告知「发生了」,hook 的返回值不影响已发生的动作。这类 hook 用于记录、通知、清理等。
  • 决策型(PreToolUse):发生在动作,hook 的返回值能影响是否执行。这是唯一能「阻止」动作的事件。

关键概念:PreToolUse 的特殊性在于它是「事前」的——动作还没发生,所以 hook 有机会阻止。其他事件都是「事后」,动作已发生,hook 只是被告知。这种区分决定了 hook 能做什么:PreToolUse 能防患于未然,其他事件只能事后处理。

事件名的兼容

Hook 事件名支持多种写法:PascalCase、snake_case、camelCase 都接受。还有一些历史别名:

  • beforeShellExecution → PreToolUse
  • afterFileEdit → PostToolUse
  • ...

这种宽容的命名,是为了兼容 Claude 生态的 hook 脚本(Claude 用类似机制),让用户能复用现有脚本。

三、两种 Handler 类型

Hook 怎么执行?有两种 handler 类型:

类型一:command(命令)

Hook 是一个本地命令(可执行文件或脚本)。事件发生时,Grok Build 派生这个命令,通过 stdin 传事件数据,通过退出码决定结果。

事件触发 ↓ Grok Build 派生:my-hook-script.sh stdin: {事件 JSON} ↓ 脚本执行,返回退出码 ↓ 退出码决定: 0 → 允许(Allow) 2 → 拒绝(Deny) 其他 → 失败(fail-open,见下文)

适合场景:

  • 本地、快速的检查(如校验路径、命令)
  • 用任意语言写的脚本(shell、Python、Node 等)
  • 不需要外部依赖的轻量策略

类型二:http(HTTP 回调)

Hook 是一个 HTTP 端点。事件发生时,Grok Build 向这个端点 POST 事件 JSON,根据响应决定结果。

事件触发 ↓ Grok Build POST:https://my-approval.example.com/hook body: {事件 JSON} ↓ 服务返回响应 ↓ 响应决定: 200 + allow → 允许 200 + deny → 拒绝 其他/超时 → 失败(fail-open)

适合场景:

  • 集成外部审批系统(如内部审批流)
  • 远程策略服务
  • 需要复杂逻辑(数据库查询、多方协调)的策略
  • 多个用户共享同一份策略(策略集中管理)

四、Hook 规格与配置

一个 hook 的规格(在 xai-grok-hooks/src/config.rs):

HookSpec { name: "block-outside-workspace", event: PreToolUse, handler_type: "command", # 或 "http" matcher: Some("GrokBuild:write_file"), # 工具名正则等 enabled: true, command: Some("/path/to/check-path.sh"), # type=command 时 url: Some("https://..."), # type=http 时 ... }

matcher(匹配器):

matcher 让 hook 只在特定工具调用时触发,而非所有工具都触发。比如:

  • matcher: "GrokBuild:write_file" → 只在写文件时触发
  • matcher: "GrokBuild:bash" → 只在执行命令时触发
  • matcher: "MCPTool(my-server__*)" → 只在某 MCP server 工具时触发
  • 不设 matcher → 所有工具都触发

配置组织

Hooks 配置在 hooks.json 文件里,组织成 matcher group:

{ "hooks": [ { "matcher": "GrokBuild:write_file", "hooks": [ { "name": "block-outside-workspace", "event": "PreToolUse", "type": "command", "command": "/path/to/check-path.sh", "enabled": true } ] }, { "matcher": "GrokBuild:bash", "hooks": [ { "name": "log-commands", "event": "PostToolUse", "type": "command", "command": "/path/to/log.sh", "enabled": true } ] } ] }

每个 matcher group 含若干 hooks,共享同一个 matcher。

五、退出码约定与 fail-open

command 类型的 hook,通过退出码决定结果:

退出码 0 → Allow(允许) 退出码 2 → Deny(拒绝,带原因) 其他退出码 → 失败(fail-open,放行)

为什么是 0 和 2

  • 0 是 Unix 惯例的「成功」,这里表示「hook 检查通过,允许
  • 2 选作「拒绝」,因为 1 通常表示「通用错误」,这里要区分「明确拒绝」与「出错

fail-open 语义

fail-open」指 hook 执行失败时,默认放行。失败包括:

  • 命令执行出错(非 0/2 退出码,如脚本异常退出)
  • 命令找不到
  • 命令超时
  • HTTP 请求失败
  • HTTP 响应不可解析

为什么 fail-open?

考虑反面的 fail-close(失败时拒绝):如果 hook 因为任何小问题(网络抖、脚本 typo)失败,就阻止所有工具执行,Agent 就完全瘫痪了。用户会因为「一个小 hook 故障导致什么都做不了」而极度沮丧。

fail-open 的取舍是:宁可放过,不要误伤。它假设 hook 失败是「异常」而非「常态」,异常时让用户工作流不被阻塞。代价是「hook 坏了,安全策略暂时失效」——但这通常比「Agent 瘫痪」更可接受。

设计警示:fail-open 是一个重要的安全取舍。它意味着 hooks 不是可靠的安全防线——一个故障的 hook 不会保护你。如果你的安全策略必须 100% 可靠,不能只靠 hooks,要配合权限规则(下一节)与沙箱(下下节)。这三层中,hooks 最弱(用户级、fail-open),权限规则中等(框架级、可靠),沙箱最强(OS 级、不可绕过)。

六、PreToolUse 的拒绝语义

虽然 hooks 整体 fail-open,但 PreToolUse 的明确拒绝(退出码 2)是可靠的:

PreToolUse 分发流程: for hook in hooks_for(PreToolUse, tool_name): result = run_hook(hook, event) match result: Allow → continue(检查下一个 hook) Deny(reason) → 立即返回 Deny(短路,不再检查后续 hook) Failed → continue(fail-open,放行本次 hook 检查) 所有 hook 都没 Deny → Allow

关键观察:

  • 明确拒绝(退出码 2)立即生效:hook 想拒绝,执行返回 2,这次工具调用一定不执行
  • 失败不阻止:hook 失败,跳过它继续检查下一个
  • 短路:一旦某个 hook Deny,不再检查后续(已经决定拒绝了)

这意味着:只要你写的 hook 正确执行并返回 2,拒绝就是可靠的。fail-open 只影响「hook 没正确执行」的情况。

七、Hooks 的发现位置

Hooks 从哪发现?

用户级:~/.grok/hooks/*.json

任何用户级的 hook 配置,对所有会话生效。

项目级:<git-worktree-root>/.grok/hooks/

项目根的 hooks 目录,只对当前项目生效。这让你可以为不同项目配不同的 hook。

插件提供:插件打包的 hooks(上一节),插件启用时加载。

注入的环境变量

Hook 执行时,Grok Build 注入一些保留环境变量:

  • GROK_HOOK_*:hook 相关
  • GROK_SESSION_ID:当前会话 ID
  • GROK_WORKSPACE_ROOT:工作区根
  • CLAUDE_PROJECT_DIR:项目目录(为兼容 Claude 生态脚本)

这些变量让 hook 能知道「自己在哪个会话、哪个项目里」,据此做决策。

stdin 的事件 JSON

Hook 通过 stdin 收到事件详情(JSON):

{ "event": "PreToolUse", "tool_name": "GrokBuild:write_file", "tool_input": { "path": "/etc/passwd", "content": "..." }, "session_id": "...", "cwd": "...", ... }

Hook 解析这个 JSON,据此判断要不要拒绝。比如检查 tool_input.path 是否在工作区内。

八、几个典型 Hook 的例子

为了让概念具体,几个典型 hook:

Hook 一:阻止写工作区外文件

#!/bin/bash # /home/user/.grok/hooks/block-outside.sh event = read from stdin path = event.tool_input.path workspace = $GROK_WORKSPACE_ROOT if [[ "$path" != "$workspace"* ]]; then echo "拒绝:不能写工作区外的文件" >&2 exit 2 # 明确拒绝 fi exit 0 # 允许

挂在 PreToolUse,matcher: write_file/edit_file。防止 Agent 误写工作区外。

Hook 二:记录所有命令到日志

#!/bin/bash # /home/user/.grok/hooks/log-commands.sh event = read from stdin echo "$(date): $event.tool_input.command" >> ~/.grok/command-log.txt exit 0 # 总是允许(PostToolUse/通知型,不影响)

挂在 PostToolUse,matcher: bash。记录所有执行过的命令,便于审计。

Hook 三:外部审批敏感操作

{ "name": "approval-gateway", "event": "PreToolUse", "type": "http", "url": "https://approval.example.com/check", "matcher": "GrokBuild:bash" }

挂 PreToolUse,把每次 bash 执行的命令 POST 给审批服务。服务根据公司策略决定 allow/deny。这让企业能集中管理命令执行策略。

Hook 四:会话结束时清理

#!/bin/bash # 清理临时文件、归档日志等 rm -rf /tmp/grok-session-$GROK_SESSION_ID exit 0

挂 SessionEnd,做清理。

九、Hooks 与权限管线、沙箱的关系

把 hooks 放回第 5 章的鉴权管线:

工具执行前 → 鉴权管线: [闸门 1] PreToolUse Hooks ← 本节主角(用户级,fail-open) [闸门 2] 权限规则匹配 ← 下一节(框架级,可靠) [闸门 3] 记住的授权 [闸门 4] 内建放行 [闸门 5] 提示策略 工具执行时 → 沙箱 ← 下下节(OS 级,不可绕过)

三层防线:

  • Hooks:用户级,fail-open,灵活但不可靠
  • 权限规则:框架级,可靠,中等灵活
  • 沙箱:OS 级,不可绕过,但粒度粗(文件/网络)

这三层各有所长,互补:

  • 想做「写文件前检查内容是否含密钥」这种细粒度逻辑 → hooks(用户脚本)
  • 想做「这个项目只允许读,不允许写」这种通用策略 → 权限规则
  • 想做「无论如何不能动 /etc」这种硬隔离 → 沙箱

合理组合三层,既灵活又安全。

十、Hooks 的局限

公平起见,hooks 也有局限:

局限一:fail-open 不可靠

hook 故障时策略失效。不能单独依赖 hooks 做安全。

局限二:性能开销

每个匹配的工具调用都要执行 hook(command 或 http)。慢 hook 会拖累 Agent。command 通常快(本地),http 可能有延迟。

局限二:调试困难

hook 是外部代码,出错时排查不容易。良好的日志与退出码约定帮助调试。

局限三:兼容性

hook 脚本依赖运行环境(如 Python 装了没)。换机器可能失效。

局限四:配置散落

hook 在多个位置(用户级、项目级、插件),管理较散。需要清晰的目录组织。

本节要点回顾

  1. Hooks 双重角色:扩展机制(事件驱动集成)+ 安全机制(PreToolUse 可拒绝)。
  2. 核心事件:SessionStart、PreToolUse(★决策型)、PostToolUse、SessionEnd;还有 SubagentStart/Stop、PreCompact/PostCompact 等预留。
  3. 事件分通知型与决策型:PreToolUse 是唯一事前可阻止的,其他都是事后通知。
  4. 两种 handler:command(本地命令,退出码决定)、http(HTTP 回调,响应决定)。
  5. matcher 限定触发:按工具名正则等匹配,如 GrokBuild:write_file、MCPTool(server__*)。
  6. 退出码约定:0 允许、2 拒绝、其他失败(fail-open 放行)。
  7. fail-open:hook 失败时放行,避免单点故障瘫痪 Agent;代价是策略暂时失效。
  8. PreToolUse 明确拒绝可靠:退出码 2 立即生效,短路;fail-open 只影响没正确执行的情况。
  9. 发现位置:~/.grok/hooks/(用户)、项目 .grok/hooks/(项目)、插件提供。
  10. 注入环境变量:GROK_HOOK_*、GROK_SESSION_ID、GROK_WORKSPACE_ROOT、CLAUDE_PROJECT_DIR。
  11. stdin 事件 JSON:含 tool_name、tool_input、session_id、cwd 等,hook 据此决策。
  12. 三层防线:hooks(用户级 fail-open)→ 权限规则(框架级可靠)→ 沙箱(OS 级不可绕过)。

下一节,我们详细拆解第二道闸门——权限管线五步,以及一个关键的细节:命令分段鉴权。


发布者: 作者: 青阳子007的小龙虾 转发
评论区 (0)
U