Hooks:生命周期钩子 本节摘要:Hooks(钩子)是 Grok Build 一个独特的机制——它让你在 Agent 的生命周期事件上挂钩,执行自定义逻辑。最典型的:在工具执行前(PreToolUse),你的 hook 可以决定要不要放行,实现自定义安全策略。Hook 可以是本地命令(command),也可以是 HTTP 回调(http),这让 hooks 既能做轻量本地检查,也能做集成外部系统的复杂逻辑。本节会讲清 hooks 的核心生命周期事件、两种 handler 类型、退出码约定与 fail-open 语义、发现位置,以及它作为「用户级安全策略」的角色。
本节摘要:Hooks(钩子)是 Grok Build 一个独特的机制——它让你在 Agent 的生命周期事件上挂钩,执行自定义逻辑。最典型的:在工具执行前(PreToolUse),你的 hook 可以决定要不要放行,实现自定义安全策略。Hook 可以是本地命令(command),也可以是 HTTP 回调(http),这让 hooks 既能做轻量本地检查,也能做集成外部系统的复杂逻辑。本节会讲清 hooks 的核心生命周期事件、两种 handler 类型、退出码约定与 fail-open 语义、发现位置,以及它作为「用户级安全策略」的角色。
Hooks 在 Grok Build 里扮演双重角色:
角色一:扩展机制
Hooks 让你「在事件发生时做点什么」。比如:
这种「事件驱动」的扩展,让外部系统能与 Grok Build 的生命周期深度集成。
角色二:安全机制
最关键的事件是 PreToolUse——它在工具执行前触发,hook 可以拒绝这次执行。这让你能实现自定义安全策略,比如:
回顾第 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。其他事件可能在后续版本启用,或作为预留。
事件的分类
这些事件大致分两类:
关键概念:PreToolUse 的特殊性在于它是「事前」的——动作还没发生,所以 hook 有机会阻止。其他事件都是「事后」,动作已发生,hook 只是被告知。这种区分决定了 hook 能做什么:PreToolUse 能防患于未然,其他事件只能事后处理。
事件名的兼容
Hook 事件名支持多种写法:PascalCase、snake_case、camelCase 都接受。还有一些历史别名:
beforeShellExecution → PreToolUseafterFileEdit → PostToolUse这种宽容的命名,是为了兼容 Claude 生态的 hook 脚本(Claude 用类似机制),让用户能复用现有脚本。
Hook 怎么执行?有两种 handler 类型:
类型一:command(命令)
Hook 是一个本地命令(可执行文件或脚本)。事件发生时,Grok Build 派生这个命令,通过 stdin 传事件数据,通过退出码决定结果。
事件触发 ↓ Grok Build 派生:my-hook-script.sh stdin: {事件 JSON} ↓ 脚本执行,返回退出码 ↓ 退出码决定: 0 → 允许(Allow) 2 → 拒绝(Deny) 其他 → 失败(fail-open,见下文)
适合场景:
类型二: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 的规格(在 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 工具时触发配置组织
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。
command 类型的 hook,通过退出码决定结果:
退出码 0 → Allow(允许) 退出码 2 → Deny(拒绝,带原因) 其他退出码 → 失败(fail-open,放行)
为什么是 0 和 2
fail-open 语义
「fail-open」指 hook 执行失败时,默认放行。失败包括:
为什么 fail-open?
考虑反面的 fail-close(失败时拒绝):如果 hook 因为任何小问题(网络抖、脚本 typo)失败,就阻止所有工具执行,Agent 就完全瘫痪了。用户会因为「一个小 hook 故障导致什么都做不了」而极度沮丧。
fail-open 的取舍是:宁可放过,不要误伤。它假设 hook 失败是「异常」而非「常态」,异常时让用户工作流不被阻塞。代价是「hook 坏了,安全策略暂时失效」——但这通常比「Agent 瘫痪」更可接受。
设计警示:fail-open 是一个重要的安全取舍。它意味着 hooks 不是可靠的安全防线——一个故障的 hook 不会保护你。如果你的安全策略必须 100% 可靠,不能只靠 hooks,要配合权限规则(下一节)与沙箱(下下节)。这三层中,hooks 最弱(用户级、fail-open),权限规则中等(框架级、可靠),沙箱最强(OS 级、不可绕过)。
虽然 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
关键观察:
这意味着:只要你写的 hook 正确执行并返回 2,拒绝就是可靠的。fail-open 只影响「hook 没正确执行」的情况。
Hooks 从哪发现?
用户级:~/.grok/hooks/*.json
任何用户级的 hook 配置,对所有会话生效。
项目级:<git-worktree-root>/.grok/hooks/
项目根的 hooks 目录,只对当前项目生效。这让你可以为不同项目配不同的 hook。
插件提供:插件打包的 hooks(上一节),插件启用时加载。
注入的环境变量
Hook 执行时,Grok Build 注入一些保留环境变量:
GROK_HOOK_*:hook 相关GROK_SESSION_ID:当前会话 IDGROK_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 一:阻止写工作区外文件
#!/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 放回第 5 章的鉴权管线:
工具执行前 → 鉴权管线: [闸门 1] PreToolUse Hooks ← 本节主角(用户级,fail-open) [闸门 2] 权限规则匹配 ← 下一节(框架级,可靠) [闸门 3] 记住的授权 [闸门 4] 内建放行 [闸门 5] 提示策略 工具执行时 → 沙箱 ← 下下节(OS 级,不可绕过)
三层防线:
这三层各有所长,互补:
合理组合三层,既灵活又安全。
公平起见,hooks 也有局限:
局限一:fail-open 不可靠
hook 故障时策略失效。不能单独依赖 hooks 做安全。
局限二:性能开销
每个匹配的工具调用都要执行 hook(command 或 http)。慢 hook 会拖累 Agent。command 通常快(本地),http 可能有延迟。
局限二:调试困难
hook 是外部代码,出错时排查不容易。良好的日志与退出码约定帮助调试。
局限三:兼容性
hook 脚本依赖运行环境(如 Python 装了没)。换机器可能失效。
局限四:配置散落
hook 在多个位置(用户级、项目级、插件),管理较散。需要清晰的目录组织。
下一节,我们详细拆解第二道闸门——权限管线五步,以及一个关键的细节:命令分段鉴权。