Appearance
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(如 BeforeTool、AfterModel),避免每次都重复计算。用文件做简单的小时级缓存:
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 配置时:
- 基于
name和command字段生成该 Hook 的唯一身份标识 - 如果是首次见到此身份,显示警告
- 执行 Hook,并将其标记为"已信任"
- 如果
command字符串被修改(如git pull带入了恶意变更),身份标识改变,CLI 再次视为新的不可信 Hook 并告警
这防止了"悄悄替换已验证命令"的供应链攻击。
主要风险
| 风险 | 说明 |
|---|---|
| 任意代码执行 | Hook 以你的身份运行,可以做任何你能做的事 |
| 数据外泄 | Hook 可以读取 Prompt 内容和环境变量(含 GEMINI_API_KEY)并发送到远端 |
| Prompt 注入 | 恶意文件或网页内容可能诱导 AI 触发高危工具 |
环境变量脱敏(强烈建议启用)
Gemini CLI 提供内置脱敏系统,自动过滤名称中含 KEY、TOKEN、SECRET 等字样的环境变量,防止 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只影响后台日志,systemMessage和reason仍会在终端显示给用户。
常见问题
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 所需变量。这样该变量可用,但其他密钥格式的变量被自动屏蔽,防止意外泄露。