Hooks 同步运行在 Agent 循环中,慢 Hook 直接拖慢每轮交互。性能关键:用 Promise.all 并行化、按小时缓存、精准 matcher 只匹配必要工具。安全关键:项目级 Hook 默认不可信、开启环境变量脱敏防止密钥泄露、脚本务必校验输入结构。

Hooks 最佳实践

本文涵盖 Gemini CLI Hooks 开发的性能优化、调试技术、安全模型和隐私配置四个核心领域。

入门教程见 Hooks 编写指南,完整 I/O Schema 见 Hooks 参考


性能优化

保持 Hook 轻快

Hook 同步运行在 Agent 循环中,每次工具调用都会等待 Hook 完成才继续。避免阻塞:

// 慢:串行请求
const data1 = await fetch(url1).then(r => r.json());
const data2 = await fetch(url2).then(r => r.json());

// 快:并行请求
const [data1, data2] = await Promise.all([
  fetch(url1).then(r => r.json()),
  fetch(url2).then(r => r.json()),
]);

缓存高开销操作

对于高频触发的 Hook(如 BeforeToolAfterModel),避免每次都重复计算。用文件做简单的小时级缓存:

const fs = require('fs');

const CACHE_FILE = '.gemini/hook-cache.json';

function readCache() {
  try { return JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')); }
  catch { return {}; }
}

function writeCache(data) {
  fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2));
}

async function main() {
  const cache = readCache();
  const cacheKey = `result-${(Date.now() / 3600000) | 0}`; // 按小时缓存

  if (cache[cacheKey]) {
    console.log(JSON.stringify(cache[cacheKey]));
    return;
  }

  const result = await computeExpensiveResult();
  cache[cacheKey] = result;
  writeCache(cache);
  console.log(JSON.stringify(result));
}
main();

选择合适的事件

事件 触发频率 推荐用途
AfterAgent 每轮一次(最终响应后) 质量验证、最终日志
AfterModel 每个流式 chunk 一次 实时脱敏、PII 过滤

如果只需检查最终输出,用 AfterAgent,而不是 AfterModel——后者在长响应中会触发几十次。

精准 Matcher

不要用 * 匹配所有工具;只匹配你真正需要的工具,避免为不相关的事件启动进程:

{
  "matcher": "write_file|replace",
  "hooks": [
    { "name": "validate-writes", "type": "command", "command": "./validate.sh" }
  ]
}

调试技术

JSON 规则:stdout 只能有一行 JSON

最常见的 Hook 失败原因是 stdout “污染”:

# 错误:echo 写到 stdout,污染 JSON
echo "正在检查..."
echo '{"decision": "allow"}'

# 正确:调试信息写到 stderr
echo "正在检查..." >&2
echo '{"decision": "allow"}'

写日志文件

Hook 在后台运行,日志文件是最简单的调试手段:

#!/usr/bin/env bash
LOG_FILE=".gemini/hooks/debug.log"

log() {
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
}

input=$(cat)
log "收到输入: ${input:0:100}..."

# Hook 逻辑...

log "Hook 完成"
echo "{}"

独立测试 Hook 脚本

在接入 CLI 之前,先用样本 JSON 独立测试:

macOS/Linux

cat > test-input.json << 'EOF'
{
  "session_id": "test-123",
  "cwd": "/tmp/test",
  "hook_event_name": "BeforeTool",
  "tool_name": "write_file",
  "tool_input": { "file_path": "test.txt", "content": "测试内容" }
}
EOF

cat test-input.json | bash .gemini/hooks/my-hook.sh
echo "Exit code: $?"

Windows(PowerShell)

@'
{
  "session_id": "test-123",
  "cwd": "C:\\temp",
  "hook_event_name": "BeforeTool",
  "tool_name": "write_file",
  "tool_input": { "file_path": "test.txt", "content": "测试内容" }
}
'@ | .\.gemini\hooks\my-hook.ps1
Write-Host "Exit code: $LASTEXITCODE"

使用 /hooks panel

/hooks panel

查看每个 Hook 的执行次数、最近成功/失败、错误信息和耗时,是排查"Hook 没有执行"的首选入口。

启用遥测

settings.json 中启用 Hook 执行日志:

{ "telemetry": { "logPrompts": true } }

然后在 GCP Logs Explorer 或本地日志文件中搜索 gemini_cli.hook_call 事件。


Hook 安全

威胁模型

Hook 来源 可信度 说明
系统层(/etc/gemini-cli/ 最高 系统管理员配置,可信
用户层(~/.gemini/ 你自己配置,你负责
扩展插件 显式安装,依赖作者可信度
项目层(./.gemini/ 默认不可信 从第三方仓库 clone 时需特别审查

项目级 Hook 的信任机制

当 Gemini CLI 发现项目目录下有 Hook 配置时:

  1. 基于 namecommand 字段生成该 Hook 的唯一身份标识
  2. 如果是首次见到此身份,显示警告
  3. 执行 Hook,并将其标记为"已信任"
  4. 如果 command 字符串被修改(如 git pull 带入了恶意变更),身份标识改变,CLI 再次视为新的不可信 Hook 并告警

这防止了"悄悄替换已验证命令"的供应链攻击。

主要风险

风险 说明
任意代码执行 Hook 以你的身份运行,可以做任何你能做的事
数据外泄 Hook 可以读取 Prompt 内容和环境变量(含 GEMINI_API_KEY)并发送到远端
Prompt 注入 恶意文件或网页内容可能诱导 AI 触发高危工具

环境变量脱敏(强烈建议启用)

Gemini CLI 提供内置脱敏系统,自动过滤名称中含 KEYTOKENSECRET 等字样的环境变量,防止 Hook 脚本意外泄露密钥。

当前默认关闭。使用第三方 Hook 或在敏感环境中工作时强烈建议开启。

{
  "security": {
    "environmentVariableRedaction": {
      "enabled": true,
      "allowed": ["MY_REQUIRED_TOOL_KEY"]
    }
  }
}

allowed 列表中的变量会例外传入 Hook(仅用于你确实需要的变量)。

Secret 扫描器示例(完整正则列表)

const SECRET_PATTERNS = [
  /api[_-]?key\s*[:=]\s*['"]?[a-zA-Z0-9_-]{20,}['"]?/i,
  /password\s*[:=]\s*['"]?[^\s'"]{8,}['"]?/i,
  /secret\s*[:=]\s*['"]?[a-zA-Z0-9_-]{20,}['"]?/i,
  /AKIA[0-9A-Z]{16}/,        // AWS Access Key
  /ghp_[a-zA-Z0-9]{36}/,    // GitHub PAT
  /sk-[a-zA-Z0-9]{48}/,     // OpenAI API Key
];

function containsSecret(content) {
  return SECRET_PATTERNS.some(p => p.test(content));
}

开发规范

description 字段记录用途

{
  "name": "secret-scanner",
  "type": "command",
  "command": "$GEMINI_PROJECT_DIR/.gemini/hooks/block-secrets.sh",
  "description": "写入文件前扫描 API Key 和密钥"
}

这段描述会显示在 /hooks panel 中,也方便后续维护。

校验输入

Hook 输入来自 LLM 或用户 Prompt,可能被篡改——永远不要盲目信任:

#!/usr/bin/env bash
input=$(cat)

# 校验 JSON 格式
if ! echo "$input" | jq empty 2>/dev/null; then
  echo "无效的 JSON 输入" >&2
  exit 1
fi

# 校验工具名只允许预期值
tool_name=$(echo "$input" | jq -r '.tool_name // empty')
if [[ "$tool_name" != "write_file" && "$tool_name" != "read_file" ]]; then
  echo "意外的工具名: $tool_name" >&2
  exit 1
fi

纳入版本控制

将 Hook 提交到仓库,与团队共享:

git add .gemini/hooks/
git add .gemini/settings.json

.gitignore 参考:

# 忽略缓存和日志
.gemini/hook-cache.json
.gemini/hook-debug.log
.gemini/memory/session-*.jsonl

# 保留脚本文件
!.gemini/hooks/*.sh
!.gemini/hooks/*.js

隐私配置

关闭 Prompt 日志

Hook 遥测可能包含 Prompt 内容(代码、文件路径等敏感信息),企业环境建议关闭:

{ "telemetry": { "logPrompts": false } }

使用 suppressOutput

单个 Hook 可以请求隐藏其元数据,避免出现在日志和遥测中:

{ "suppressOutput": true }

suppressOutput 只影响后台日志,systemMessagereason 仍会在终端显示给用户。


常见问题

Q: Hook 配置正确但就是不执行,怎么排查?

A: 按顺序检查:① /hooks panel 确认 Hook 出现在列表且未被 disabled;② 检查 matcher 正则是否能匹配目标工具名(在 shell 里单独测试);③ macOS/Linux 确认脚本有执行权限(chmod +x);④ Windows 确认 PowerShell 执行策略允许运行脚本(Get-ExecutionPolicy);⑤ 检查 settings.json 中是否有 hooks.disabled 列出了该 Hook 名。

Q: Hook 超时怎么处理?

A: 默认超时 60000ms。延长方法:在 Hook 配置中加 "timeout": 120000。建议同时优化慢操作:用缓存避免重复计算,用 Promise.all 并行化 I/O,或把耗时任务拆分到后台进程异步执行。

Q: 如何安全地在 Hook 里使用环境变量中的密钥?

A: 启用 environmentVariableRedaction,然后在 allowed 列表中明确添加你的 Hook 所需变量。这样该变量可用,但其他密钥格式的变量被自动屏蔽,防止意外泄露。