Skip to content

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

Hooks 最佳实践

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

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


性能优化

保持 Hook 轻快

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

javascript
// 慢:串行请求
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),避免每次都重复计算。用文件做简单的小时级缓存:

javascript
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

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

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

调试技术

JSON 规则:stdout 只能有一行 JSON

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

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

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

写日志文件

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

bash
#!/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

bash
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)

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

bash
/hooks panel

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

启用遥测

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

json
{ "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 或在敏感环境中工作时强烈建议开启。

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

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

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

javascript
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 字段记录用途

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

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

校验输入

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

bash
#!/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 提交到仓库,与团队共享:

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

.gitignore 参考:

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

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

隐私配置

关闭 Prompt 日志

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

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

使用 suppressOutput

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

json
{ "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 所需变量。这样该变量可用,但其他密钥格式的变量被自动屏蔽,防止意外泄露。