Appearance
Gemini CLI Hooks 通过 stdin/stdout 交换 JSON 数据,用 Shell 脚本或 Node.js 即可实现工具调用拦截、安全扫描、上下文注入和响应验证。核心规则:调试信息写 stderr,最终 JSON 才写 stdout;exit 0 输出结构化决策,exit 2 触发紧急阻断。
编写 Hooks 脚本
本文从最简单的日志脚本出发,逐步构建一套完整的开发工作流助手。开始前请确认:
- Gemini CLI 已安装并完成认证
- 了解基本 Shell 脚本或 Node.js
- 了解 JSON 格式
关于所有 Hook 事件的 I/O 格式完整参考,见 Hooks 参考文档。
快速开始:工具调用日志
核心规则:调试信息写 stderr,最终 JSON 才写 stdout。
第一步:创建 Hook 脚本
macOS/Linux
bash
mkdir -p .gemini/hooks
cat > .gemini/hooks/log-tools.sh << 'EOF'
#!/usr/bin/env bash
input=$(cat)
# 提取工具名(需要 jq)
tool_name=$(echo "$input" | jq -r '.tool_name')
# 调试信息写 stderr
echo "正在记录工具: $tool_name" >&2
# 写入日志文件
echo "[$(date)] 工具执行: $tool_name" >> .gemini/tool-log.txt
# 返回空 JSON,表示允许执行
echo "{}"
exit 0
EOF
chmod +x .gemini/hooks/log-tools.shWindows(PowerShell)
powershell
New-Item -ItemType Directory -Force -Path ".gemini\hooks"
@"
`$inputJson = `$input | Out-String | ConvertFrom-Json
`$toolName = `$inputJson.tool_name
[Console]::Error.WriteLine("正在记录工具: `$toolName")
"[`$(Get-Date -Format 'o')] 工具执行: `$toolName" | Out-File -FilePath ".gemini\tool-log.txt" -Append -Encoding utf8
"{}"
"@ | Out-File -FilePath ".gemini\hooks\log-tools.ps1" -Encoding utf8第二步:在 settings.json 注册 Hook
json
{
"hooks": {
"AfterTool": [
{
"matcher": ".*",
"hooks": [
{
"name": "log-tools",
"type": "command",
"command": "bash .gemini/hooks/log-tools.sh"
}
]
}
]
}
}第三步:验证
运行 Gemini CLI 执行任意工具后,检查 .gemini/tool-log.txt 是否有记录。
退出码策略
| 策略 | 退出码 | 实现方式 | 适合场景 |
|---|---|---|---|
| 结构化决策 | 0 | 返回 {"decision": "deny", "reason": "..."} | 生产环境、自定义反馈信息 |
| 紧急阻断 | 2 | 将错误信息写到 stderr 后 exit 2 | 简单安全门控、脚本异常 |
实用示例
安全:阻止含密钥的提交
在 BeforeTool 中扫描 write_file 操作,阻止包含 API Key 等敏感内容的写入。
.gemini/hooks/block-secrets.sh
bash
#!/usr/bin/env bash
input=$(cat)
# 提取写入内容
content=$(echo "$input" | jq -r '.tool_input.content // .tool_input.new_string // ""')
# 检测敏感内容
if echo "$content" | grep -qE 'api[_-]?key|password|secret'; then
echo "检测到潜在密钥,已拦截" >&2
cat <<EOF
{
"decision": "deny",
"reason": "安全策略:内容中检测到潜在密钥,操作已被阻止。",
"systemMessage": "🔒 安全扫描拦截了此操作"
}
EOF
exit 0
fi
echo '{"decision": "allow"}'
exit 0settings.json 配置:
json
{
"hooks": {
"BeforeTool": [
{
"matcher": "write_file",
"hooks": [
{
"name": "security",
"type": "command",
"command": "bash .gemini/hooks/block-secrets.sh"
}
]
}
]
}
}上下文注入:自动添加 Git 历史
在 BeforeAgent 中注入最近提交,让模型更了解当前代码状态。
.gemini/hooks/inject-context.sh
bash
#!/usr/bin/env bash
context=$(git log -5 --oneline 2>/dev/null || echo "暂无 git 历史")
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "BeforeAgent",
"additionalContext": "最近提交记录:\n$context"
}
}
EOF工具过滤:按意图限制可用工具(Node.js)
BeforeToolSelection 根据用户 Prompt 内容,只暴露相关工具,减少无关工具占用的 Token。
.gemini/hooks/filter-tools.js
javascript
#!/usr/bin/env node
const fs = require('fs');
async function main() {
const input = JSON.parse(fs.readFileSync(0, 'utf-8'));
const messages = (input.llm_request.messages || []);
const lastUser = messages.slice().reverse().find(m => m.role === 'user');
if (!lastUser) {
console.log(JSON.stringify({}));
return;
}
const text = lastUser.content;
const allowed = ['write_todos'];
if (text.includes('read') || text.includes('查看')) {
allowed.push('read_file', 'list_directory');
}
if (text.includes('test') || text.includes('测试')) {
allowed.push('run_shell_command');
}
if (allowed.length > 1) {
console.log(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'BeforeToolSelection',
toolConfig: {
mode: 'AUTO',
allowedFunctionNames: allowed,
},
},
}));
} else {
console.log(JSON.stringify({}));
}
}
main().catch(err => {
console.error(err);
process.exit(1);
});多个 BeforeToolSelection Hook 的合并规则:多个 Hook 的
allowedFunctionNames取并集(union),只有mode: "NONE"可以覆盖其他 Hook 禁用所有工具。
综合示例:智能开发工作流助手
以下示例将所有 Hook 事件串联,构建一个能记忆项目状态、自动扫描安全、验证响应质量的完整助手。
架构
- SessionStart:加载项目记忆
- BeforeAgent:注入记忆到上下文
- BeforeToolSelection:按意图过滤工具
- BeforeTool:安全扫描
- AfterModel:记录交互
- AfterAgent:验证响应质量(自动重试)
- SessionEnd:整合保存记忆
settings.json 完整配置
json
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [{ "name": "init", "type": "command", "command": "node .gemini/hooks/init.js" }]
}
],
"BeforeAgent": [
{
"matcher": ".*",
"hooks": [{ "name": "memory", "type": "command", "command": "node .gemini/hooks/inject-memories.js" }]
}
],
"BeforeToolSelection": [
{
"matcher": ".*",
"hooks": [{ "name": "filter", "type": "command", "command": "node .gemini/hooks/filter-tools.js" }]
}
],
"BeforeTool": [
{
"matcher": "write_file",
"hooks": [{ "name": "security", "type": "command", "command": "node .gemini/hooks/security.js" }]
}
],
"AfterModel": [
{
"matcher": ".*",
"hooks": [{ "name": "record", "type": "command", "command": "node .gemini/hooks/record.js" }]
}
],
"AfterAgent": [
{
"matcher": ".*",
"hooks": [{ "name": "validate", "type": "command", "command": "node .gemini/hooks/validate.js" }]
}
],
"SessionEnd": [
{
"matcher": "exit",
"hooks": [{ "name": "save", "type": "command", "command": "node .gemini/hooks/consolidate.js" }]
}
]
}
}关键脚本
inject-memories.js(BeforeAgent,注入项目记忆)
javascript
#!/usr/bin/env node
const fs = require('fs');
async function main() {
JSON.parse(fs.readFileSync(0, 'utf-8')); // 读取 input(此处不使用)
const memories = '- [记忆] 本项目统一使用 TypeScript。';
console.log(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'BeforeAgent',
additionalContext: `\n## 项目记忆\n${memories}`,
},
}));
}
main();security.js(BeforeTool,密钥检测)
javascript
#!/usr/bin/env node
const fs = require('fs');
const input = JSON.parse(fs.readFileSync(0));
const content = input.tool_input.content || '';
if (content.includes('SECRET_KEY')) {
console.log(JSON.stringify({
decision: 'deny',
reason: '检测到 SECRET_KEY,已阻止写入',
systemMessage: '🚨 安全拦截:敏感提交被阻断',
}));
process.exit(0);
}
console.log(JSON.stringify({ decision: 'allow' }));validate.js(AfterAgent,响应质量验证 + 自动重试)
javascript
#!/usr/bin/env node
const fs = require('fs');
const input = JSON.parse(fs.readFileSync(0));
const response = input.prompt_response;
if (!response.includes('Summary:')) {
console.log(JSON.stringify({
decision: 'block', // 触发自动重试
reason: '响应缺少 Summary 部分,请补充。',
systemMessage: '🔄 要求补充摘要...',
}));
process.exit(0);
}
console.log(JSON.stringify({ decision: 'allow' }));常见问题
Q: Hook 脚本里的 echo 输出影响了 stdout,CLI 报解析错误怎么办?
A: 将所有调试 echo 改为 echo ... >&2 写到 stderr。stdout 只能有一行 JSON,多余任何字符(哪怕空格)都会导致解析失败。
Q: 如何让同一个 Hook 文件同时处理多种工具?
A: 使用正则 matcher,例如 "matcher": "write_file|run_shell_command",然后在脚本内部用 hook_event_name 和 tool_name 字段区分分支。
Q: BeforeToolSelection 的 mode: "NONE" 和 allowedFunctionNames: [] 有什么区别?
A: mode: "NONE" 会禁用所有工具且优先级最高,覆盖同一 Hook 组的其他允许规则;allowedFunctionNames: [] 本身不表示禁用,只是空白名单(可能被合并后的其他 Hook 覆盖)。要彻底禁用所有工具,必须用 mode: "NONE"。