Gemini CLI 上的 Hooks:最佳实践
Gemini CLI 上的 Hooks:最佳实践
Section titled “Gemini CLI 上的 Hooks:最佳实践”本指南涵盖了在 Gemini CLI 中开发和部署 hooks 的安全性考虑、性能优化、调试技巧以及隐私考虑。
保持 hooks 的快速执行
Section titled “保持 hooks 的快速执行”Hooks 是同步运行的——慢速 hooks 会延迟代理循环。通过使用并行操作来优化速度:
// Sequential operations are slowerconst data1 = await fetch(url1).then((r) => r.json());const data2 = await fetch(url2).then((r) => r.json());const data3 = await fetch(url3).then((r) => r.json());
// Prefer parallel operations for better performance// Start requests concurrentlyconst p1 = fetch(url1).then((r) => r.json());const p2 = fetch(url2).then((r) => r.json());const p3 = fetch(url3).then((r) => r.json());
// Wait for all resultsconst [data1, data2, data3] = await Promise.all([p1, p2, p3]);缓存昂贵的操作
Section titled “缓存昂贵的操作”在调用之间存储结果,以避免重复的计算:
const fs = require('fs');const path = require('path');
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 = `tool-list-${(Date.now() / 3600000) | 0}`; // Hourly cache
if (cache[cacheKey]) { console.log(JSON.stringify(cache[cacheKey])); return; }
// Expensive operation const result = await computeExpensiveResult(); cache[cacheKey] = result; writeCache(cache);
console.log(JSON.stringify(result));}使用合适的事件
Section titled “使用合适的事件”选择与您的用例相匹配的 hook 事件,以避免不必要的执行。AfterAgent 每次代理循环完成时触发一次,而 AfterModel 在每次 LLM 调用后触发(可能每次循环多次):
// If checking final completion, use AfterAgent instead of AfterModel{ "hooks": { "AfterAgent": [ { "matcher": "*", "hooks": [ { "name": "final-checker", "command": "./check-completion.sh" } ] } ] }}使用匹配器进行过滤
Section titled “使用匹配器进行过滤”使用特定的匹配器以避免不必要的 hook 执行。不要使用 * 匹配所有工具,只需指定您需要的工具:
{ "matcher": "write_file|replace", "hooks": [ { "name": "validate-writes", "command": "./validate.sh" } ]}优化 JSON 解析
Section titled “优化 JSON 解析”对于大型输入,使用流式 JSON 解析器以避免将所有内容加载到内存中:
// Standard approach: parse entire inputconst input = JSON.parse(await readStdin());const content = input.tool_input.content;
// For very large inputs: stream and extract only needed fieldsconst { createReadStream } = require('fs');const JSONStream = require('JSONStream');
const stream = createReadStream(0).pipe(JSONStream.parse('tool_input.content'));let content = '';stream.on('data', (chunk) => { content += chunk;});将日志写入文件
Section titled “将日志写入文件”将调试信息写入专用的日志文件:
#!/usr/bin/env bashLOG_FILE=".gemini/hooks/debug.log"
# Log with timestamplog() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"}
input=$(cat)log "Received input: ${input:0:100}..."
# Hook logic here
log "Hook completed successfully"使用 stderr 输出错误
Section titled “使用 stderr 输出错误”根据退出代码,stderr 上的错误消息会被适当地展示:
try { const result = dangerousOperation(); console.log(JSON.stringify({ result }));} catch (error) { console.error(`Hook error: ${error.message}`); process.exit(2); // Blocking error}独立测试 hooks
Section titled “独立测试 hooks”手动运行 hook 脚本,并使用示例 JSON 输入:
# Create test inputcat > 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": "Test content" }}EOF
# Test the hookcat test-input.json | .gemini/hooks/my-hook.sh
# Check exit codeecho "Exit code: $?"检查退出代码
Section titled “检查退出代码”确保您的脚本返回正确的退出代码:
#!/usr/bin/env bashset -e # Exit on error
# Hook logicprocess_input() { # ...}
if process_input; then echo "Success message" exit 0else echo "Error message" >&2 exit 2fi当 telemetry.logPrompts 被启用时,会记录 hook 的执行情况:
{ "telemetry": { "logPrompts": true }}查看日志中的 hook 遥测数据,以调试执行问题。
使用 hook 面板
Section titled “使用 hook 面板”/hooks panel 命令显示执行状态和最近的输出:
/hooks panel检查以下内容:
- Hook 执行计数
- 近期的成功/失败
- 错误消息
- 执行时间
在实现复杂逻辑之前,先从基本的日志 hook 开始:
#!/usr/bin/env bash# Simple logging hook to understand input structureinput=$(cat)echo "$input" >> .gemini/hook-inputs.logecho "Logged input"使用 JSON 库
Section titled “使用 JSON 库”【要求】:只返回翻译后的中文文本,不要添加任何解释或注释。
【译文】
解析 JSON 时应使用适当的库,而不是文本处理:
错误的做法:
# Fragile text parsingtool_name=$(echo "$input" | grep -oP '"tool_name":\s*"\K[^"]+')正确的做法:
# Robust JSON parsingtool_name=$(echo "$input" | jq -r '.tool_name')设置脚本可执行
Section titled “设置脚本可执行”始终确保钩子脚本可执行:
chmod +x .gemini/hooks/*.shchmod +x .gemini/hooks/*.js提交钩子以与团队共享:
git add .gemini/hooks/git add .gemini/settings.jsongit commit -m "Add project hooks for security and testing".gitignore 注意事项:
# Ignore hook cache and logs.gemini/hook-cache.json.gemini/hook-debug.log.gemini/memory/session-*.jsonl
# Keep hook scripts!.gemini/hooks/*.sh!.gemini/hooks/*.js添加描述以帮助他人理解你的钩子:
{ "hooks": { "BeforeTool": [ { "matcher": "write_file|replace", "hooks": [ { "name": "secret-scanner", "type": "command", "command": "$GEMINI_PROJECT_DIR/.gemini/hooks/block-secrets.sh", "description": "Scans code changes for API keys, passwords, and other secrets before writing" } ] } ] }}在钩子脚本中添加注释:
#!/usr/bin/env node/** * RAG Tool Filter Hook * * This hook reduces the tool space from 100+ tools to ~15 relevant ones * by extracting keywords from the user's request and filtering tools * based on semantic similarity. * * Performance: ~500ms average, cached tool embeddings * Dependencies: @google/generative-ai */检查 /hooks panel 中的钩子名称:
/hooks panel验证钩子是否出现在列表中并已启用。
验证匹配模式:
# Test regex patternecho "write_file|replace" | grep -E "write_.*|replace"检查禁用列表:
{ "hooks": { "disabled": ["my-hook-name"] }}确保脚本可执行:
ls -la .gemini/hooks/my-hook.shchmod +x .gemini/hooks/my-hook.sh验证脚本路径:
# Check path expansionecho "$GEMINI_PROJECT_DIR/.gemini/hooks/my-hook.sh"
# Verify file existstest -f "$GEMINI_PROJECT_DIR/.gemini/hooks/my-hook.sh" && echo "File exists"检查配置的超时时间:
{ "name": "slow-hook", "timeout": 60000}优化缓慢操作:
// Before: Sequential operations (slow)for (const item of items) { await processItem(item);}
// After: Parallel operations (fast)await Promise.all(items.map((item) => processItem(item)));使用缓存:
const cache = new Map();
async function getCachedData(key) { if (cache.has(key)) { return cache.get(key); } const data = await fetchData(key); cache.set(key, data); return data;}考虑将其拆分为多个更快的钩子:
{ "hooks": { "BeforeTool": [ { "matcher": "write_file", "hooks": [ { "name": "quick-check", "command": "./quick-validation.sh", "timeout": 1000 } ] }, { "matcher": "write_file", "hooks": [ { "name": "deep-check", "command": "./deep-analysis.sh", "timeout": 30000 } ] } ] }}无效的 JSON 输出
Section titled “无效的 JSON 输出”在输出前验证 JSON:
#!/usr/bin/env bashoutput='{"decision": "allow"}'
# Validate JSONif echo "$output" | jq empty 2>/dev/null; then echo "$output"else echo "Invalid JSON generated" >&2 exit 1fi确保正确的引号和转义:
// Bad: Unescaped string interpolationconst message = `User said: ${userInput}`;console.log(JSON.stringify({ message }));
// Good: Automatic escapingconsole.log(JSON.stringify({ message: `User said: ${userInput}` }));检查是否有二进制数据或控制字符:
function sanitizeForJSON(str) { return str.replace(/[\x00-\x1F\x7F-\x9F]/g, ''); // Remove control chars}
const cleanContent = sanitizeForJSON(content);console.log(JSON.stringify({ content: cleanContent }));验证脚本是否返回正确的代码:
#!/usr/bin/env bashset -e # Exit on error
# Processing logicif validate_input; then echo "Success" exit 0else echo "Validation failed" >&2 exit 2fi检查是否有未意的错误:
#!/usr/bin/env bash# Don't use 'set -e' if you want to handle errors explicitly# set -e
if ! command_that_might_fail; then # Handle error echo "Command failed but continuing" >&2fi
# Always exit explicitlyexit 0使用 trap 进行清理:
#!/usr/bin/env bash
cleanup() { # Cleanup logic rm -f /tmp/hook-temp-*}
trap cleanup EXIT
# Hook logic here环境变量不可用
Section titled “环境变量不可用”检查变量是否已设置:
#!/usr/bin/env bash
if [ -z "$GEMINI_PROJECT_DIR" ]; then echo "GEMINI_PROJECT_DIR not set" >&2 exit 1fi
if [ -z "$CUSTOM_VAR" ]; then echo "Warning: CUSTOM_VAR not set, using default" >&2 CUSTOM_VAR="default-value"fi调试可用的变量:
#!/usr/bin/env bash
# List all environment variablesenv > .gemini/hook-env.log
# Check specific variablesecho "GEMINI_PROJECT_DIR: $GEMINI_PROJECT_DIR" >> .gemini/hook-env.logecho "GEMINI_SESSION_ID: $GEMINI_SESSION_ID" >> .gemini/hook-env.logecho "GEMINI_API_KEY: ${GEMINI_API_KEY:+<set>}" >> .gemini/hook-env.log使用 .env 文件:
#!/usr/bin/env bash
# Load .env file if it existsif [ -f "$GEMINI_PROJECT_DIR/.env" ]; then source "$GEMINI_PROJECT_DIR/.env"fi安全使用钩子
Section titled “安全使用钩子”理解钩子的来源以及它们能做什么对于安全使用至关重要。
| 挂钩源 | 描述 |
|---|---|
| 系统 | 由系统管理员配置(例如,/etc/gemini-cli/settings.json,/Library/...)。被认为是最安全的。 |
用户 (~/.gemini/...) | 由您配置。您负责确保它们是安全的。 |
| 扩展 | 您明确批准并安装这些扩展。安全性取决于扩展来源(完整性)。 |
项目 (./.gemini/...) | **默认不信任。**在受信任的内部仓库中最安全;在第三方/公共仓库中风险较高。 |
项目挂钩安全性
Section titled “项目挂钩安全性”当您打开在.gemini/settings.json中定义了挂钩的项目时:
- 检测:Gemini CLI 会检测到挂钩。
- 识别:基于每个挂钩的
name和command生成唯一标识。 - 警告:如果此特定挂钩标识之前未出现过,将显示警告。
- 执行:执行挂钩(除非特定的安全设置阻止它)。
- 信任:将挂钩标记为此项目的“受信任”。
[!重要] 修改检测:如果项目挂钩的
command字符串发生变化(例如,通过git pull),其标识将改变。Gemini CLI 会将其视为新的、不受信任的挂钩并再次警告您。这防止了恶意行为者悄无声息地将经过验证的命令替换为恶意的命令。
| 【风险】 | 【描述】 |
|---|---|
| 任意代码执行 | 钩子以您的用户身份运行。它们可以做任何您能做的事情(删除文件,安装软件)。 |
| 数据泄露 | 一个钩子可以读取您的输入(提示),输出(代码)或环境变量(GEMINI_API_KEY)并将它们发送到远程服务器。 |
| 提示注入 | 文件或网页中的恶意内容可能欺骗大型语言模型运行一个工具,以意想不到的方式触发一个钩子。 |
在启用任何项目钩子或扩展之前,核实它们的来源。
- 对于开源项目,建议快速审查钩子脚本。
- 对于扩展,确保您信任作者或发布者(例如,经过验证的发布者,知名社区成员)。
- 对于来自未知来源的混淆脚本或编译后的二进制文件要谨慎。
钩子继承了 Gemini CLI 进程的环境,其中可能包括敏感的 API 密钥。Gemini CLI 尝试清洗敏感变量,但您应该保持警惕。
- 除非必要,否则避免将环境变量打印到 stdout/stderr。
- 使用
.env文件来安全地管理敏感变量,确保它们被排除在版本控制之外。
**系统管理员:**您可以在系统配置中默认执行环境变量编辑(例如,/etc/gemini-cli/settings.json):
{ "security": { "environmentVariableRedaction": { "enabled": true, "blocked": ["MY_SECRET_KEY"], "allowed": ["SAFE_VAR"] } }}编写安全的钩子
Section titled “编写安全的钩子”在编写自己的钩子时,请遵循以下实践以确保它们健壮且安全。
验证所有输入
Section titled “验证所有输入”【翻译】 永远不要在未经验证的情况下信任来自钩子的数据。钩子的输入通常来自LLM或用户提示,这些可能会被操纵。
通过设置超时,防止拒绝服务(挂起的代理)。Gemini CLI默认为60秒,但对于快速钩子,您应该设置更严格的限制。
以最小的必要权限运行钩子:
示例:秘密扫描器
Section titled “示例:秘密扫描器”使用BeforeTool钩子防止提交敏感数据。这是增强您工作流程安全性的一种强大模式。
钩子的输入和输出可能包含敏感信息。Gemini CLI尊重钩子数据日志的telemetry.logPrompts设置。
钩子遥测可能包括:
- 钩子输入: 用户提示、工具参数、文件内容
- 钩子输出: 钩子响应、决策原因、添加的上下文
- 标准流: 来自钩子进程的stdout和stderr
- 执行元数据: 钩子名称、事件类型、持续时间、成功/失败
启用(默认):
完整的钩子I/O被记录到遥测中。在以下情况下使用此设置:
- 开发和调试钩子
- 遥测被重定向到受信任的企业系统
- 您了解并接受隐私影响
禁用:
仅记录元数据(事件名称、持续时间、成功/失败)。钩子输入和输出被排除在外。在以下情况下使用此设置:
- 向第三方系统发送遥测
- 处理敏感数据
- 隐私法规要求最小化数据收集
在设置中禁用PII日志记录:
{ "telemetry": { "logPrompts": false }}通过环境变量禁用:
export GEMINI_TELEMETRY_LOG_PROMPTS=false钩子中的敏感数据
Section titled “钩子中的敏感数据”如果您的钩子处理敏感数据:
- 最小化日志记录: 不要将敏感数据写入日志文件
- 清理输出: 在输出前移除敏感数据
- 使用安全存储: 对静止的敏感数据进行加密
- 限制访问: 限制钩子脚本权限
清理示例:
function sanitizeOutput(data) { const sanitized = { ...data };
// Remove sensitive fields delete sanitized.apiKey; delete sanitized.password;
// Redact sensitive strings if (sanitized.content) { sanitized.content = sanitized.content.replace( /api[_-]?key\s*[:=]\s*['"]?[a-zA-Z0-9_-]{20,}['"]?/gi, '[REDACTED]', ); }
return sanitized;}
console.log(JSON.stringify(sanitizeOutput(hookOutput)));