Skip to content

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.sh

Windows(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 0

settings.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 事件串联,构建一个能记忆项目状态、自动扫描安全、验证响应质量的完整助手。

架构

  1. SessionStart:加载项目记忆
  2. BeforeAgent:注入记忆到上下文
  3. BeforeToolSelection:按意图过滤工具
  4. BeforeTool:安全扫描
  5. AfterModel:记录交互
  6. AfterAgent:验证响应质量(自动重试)
  7. 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_nametool_name 字段区分分支。

Q: BeforeToolSelection 的 mode: "NONE"allowedFunctionNames: [] 有什么区别?

A: mode: "NONE" 会禁用所有工具且优先级最高,覆盖同一 Hook 组的其他允许规则;allowedFunctionNames: [] 本身不表示禁用,只是空白名单(可能被合并后的其他 Hook 覆盖)。要彻底禁用所有工具,必须用 mode: "NONE"