跳转到内容

Gemini CLI 上的 Hooks:最佳实践

本指南涵盖了在 Gemini CLI 中开发和部署 hooks 的安全性考虑、性能优化、调试技巧以及隐私考虑。

Hooks 是同步运行的——慢速 hooks 会延迟代理循环。通过使用并行操作来优化速度:

// Sequential operations are slower
const 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 concurrently
const 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 results
const [data1, data2, data3] = await Promise.all([p1, p2, p3]);

在调用之间存储结果,以避免重复的计算:

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));
}

选择与您的用例相匹配的 hook 事件,以避免不必要的执行。AfterAgent 每次代理循环完成时触发一次,而 AfterModel 在每次 LLM 调用后触发(可能每次循环多次):

// If checking final completion, use AfterAgent instead of AfterModel
{
"hooks": {
"AfterAgent": [
{
"matcher": "*",
"hooks": [
{
"name": "final-checker",
"command": "./check-completion.sh"
}
]
}
]
}
}

使用特定的匹配器以避免不必要的 hook 执行。不要使用 * 匹配所有工具,只需指定您需要的工具:

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

对于大型输入,使用流式 JSON 解析器以避免将所有内容加载到内存中:

// Standard approach: parse entire input
const input = JSON.parse(await readStdin());
const content = input.tool_input.content;
// For very large inputs: stream and extract only needed fields
const { 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;
});

将调试信息写入专用的日志文件:

#!/usr/bin/env bash
LOG_FILE=".gemini/hooks/debug.log"
# Log with timestamp
log() {
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 上的错误消息会被适当地展示:

try {
const result = dangerousOperation();
console.log(JSON.stringify({ result }));
} catch (error) {
console.error(`Hook error: ${error.message}`);
process.exit(2); // Blocking error
}

手动运行 hook 脚本,并使用示例 JSON 输入:

Terminal window
# Create test input
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": "Test content"
}
}
EOF
# Test the hook
cat test-input.json | .gemini/hooks/my-hook.sh
# Check exit code
echo "Exit code: $?"

确保您的脚本返回正确的退出代码:

#!/usr/bin/env bash
set -e # Exit on error
# Hook logic
process_input() {
# ...
}
if process_input; then
echo "Success message"
exit 0
else
echo "Error message" >&2
exit 2
fi

telemetry.logPrompts 被启用时,会记录 hook 的执行情况:

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

查看日志中的 hook 遥测数据,以调试执行问题。

/hooks panel 命令显示执行状态和最近的输出:

Terminal window
/hooks panel

检查以下内容:

  • Hook 执行计数
  • 近期的成功/失败
  • 错误消息
  • 执行时间

在实现复杂逻辑之前,先从基本的日志 hook 开始:

#!/usr/bin/env bash
# Simple logging hook to understand input structure
input=$(cat)
echo "$input" >> .gemini/hook-inputs.log
echo "Logged input"

【要求】:只返回翻译后的中文文本,不要添加任何解释或注释。

【译文】

解析 JSON 时应使用适当的库,而不是文本处理:

错误的做法:

Terminal window
# Fragile text parsing
tool_name=$(echo "$input" | grep -oP '"tool_name":\s*"\K[^"]+')

正确的做法:

Terminal window
# Robust JSON parsing
tool_name=$(echo "$input" | jq -r '.tool_name')

始终确保钩子脚本可执行:

Terminal window
chmod +x .gemini/hooks/*.sh
chmod +x .gemini/hooks/*.js

提交钩子以与团队共享:

Terminal window
git add .gemini/hooks/
git add .gemini/settings.json
git 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 中的钩子名称:

Terminal window
/hooks panel

验证钩子是否出现在列表中并已启用。

验证匹配模式:

Terminal window
# Test regex pattern
echo "write_file|replace" | grep -E "write_.*|replace"

检查禁用列表:

{
"hooks": {
"disabled": ["my-hook-name"]
}
}

确保脚本可执行:

Terminal window
ls -la .gemini/hooks/my-hook.sh
chmod +x .gemini/hooks/my-hook.sh

验证脚本路径:

Terminal window
# Check path expansion
echo "$GEMINI_PROJECT_DIR/.gemini/hooks/my-hook.sh"
# Verify file exists
test -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:

#!/usr/bin/env bash
output='{"decision": "allow"}'
# Validate JSON
if echo "$output" | jq empty 2>/dev/null; then
echo "$output"
else
echo "Invalid JSON generated" >&2
exit 1
fi

确保正确的引号和转义:

// Bad: Unescaped string interpolation
const message = `User said: ${userInput}`;
console.log(JSON.stringify({ message }));
// Good: Automatic escaping
console.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 bash
set -e # Exit on error
# Processing logic
if validate_input; then
echo "Success"
exit 0
else
echo "Validation failed" >&2
exit 2
fi

检查是否有未意的错误:

#!/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" >&2
fi
# Always exit explicitly
exit 0

使用 trap 进行清理:

#!/usr/bin/env bash
cleanup() {
# Cleanup logic
rm -f /tmp/hook-temp-*
}
trap cleanup EXIT
# Hook logic here

检查变量是否已设置:

#!/usr/bin/env bash
if [ -z "$GEMINI_PROJECT_DIR" ]; then
echo "GEMINI_PROJECT_DIR not set" >&2
exit 1
fi
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 variables
env > .gemini/hook-env.log
# Check specific variables
echo "GEMINI_PROJECT_DIR: $GEMINI_PROJECT_DIR" >> .gemini/hook-env.log
echo "GEMINI_SESSION_ID: $GEMINI_SESSION_ID" >> .gemini/hook-env.log
echo "GEMINI_API_KEY: ${GEMINI_API_KEY:+<set>}" >> .gemini/hook-env.log

使用 .env 文件:

#!/usr/bin/env bash
# Load .env file if it exists
if [ -f "$GEMINI_PROJECT_DIR/.env" ]; then
source "$GEMINI_PROJECT_DIR/.env"
fi

理解钩子的来源以及它们能做什么对于安全使用至关重要。

挂钩源描述
系统由系统管理员配置(例如,/etc/gemini-cli/settings.json/Library/...)。被认为是最安全的。
用户 (~/.gemini/...)由您配置。您负责确保它们是安全的。
扩展您明确批准并安装这些扩展。安全性取决于扩展来源(完整性)。
项目 (./.gemini/...)**默认不信任。**在受信任的内部仓库中最安全;在第三方/公共仓库中风险较高。

当您打开在.gemini/settings.json中定义了挂钩的项目时:

  1. 检测:Gemini CLI 会检测到挂钩。
  2. 识别:基于每个挂钩的namecommand生成唯一标识。
  3. 警告:如果此特定挂钩标识之前未出现过,将显示警告
  4. 执行:执行挂钩(除非特定的安全设置阻止它)。
  5. 信任:将挂钩标记为此项目的“受信任”。

[!重要] 修改检测:如果项目挂钩的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"]
}
}
}

在编写自己的钩子时,请遵循以下实践以确保它们健壮且安全。

【翻译】 永远不要在未经验证的情况下信任来自钩子的数据。钩子的输入通常来自LLM或用户提示,这些可能会被操纵。

通过设置超时,防止拒绝服务(挂起的代理)。Gemini CLI默认为60秒,但对于快速钩子,您应该设置更严格的限制。

以最小的必要权限运行钩子:

使用BeforeTool钩子防止提交敏感数据。这是增强您工作流程安全性的一种强大模式。

钩子的输入和输出可能包含敏感信息。Gemini CLI尊重钩子数据日志的telemetry.logPrompts设置。

钩子遥测可能包括:

  • 钩子输入: 用户提示、工具参数、文件内容
  • 钩子输出: 钩子响应、决策原因、添加的上下文
  • 标准流: 来自钩子进程的stdout和stderr
  • 执行元数据: 钩子名称、事件类型、持续时间、成功/失败

启用(默认):

完整的钩子I/O被记录到遥测中。在以下情况下使用此设置:

  • 开发和调试钩子
  • 遥测被重定向到受信任的企业系统
  • 您了解并接受隐私影响

禁用:

仅记录元数据(事件名称、持续时间、成功/失败)。钩子输入和输出被排除在外。在以下情况下使用此设置:

  • 向第三方系统发送遥测
  • 处理敏感数据
  • 隐私法规要求最小化数据收集

在设置中禁用PII日志记录:

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

通过环境变量禁用:

Terminal window
export GEMINI_TELEMETRY_LOG_PROMPTS=false

如果您的钩子处理敏感数据:

  1. 最小化日志记录: 不要将敏感数据写入日志文件
  2. 清理输出: 在输出前移除敏感数据
  3. 使用安全存储: 对静止的敏感数据进行加密
  4. 限制访问: 限制钩子脚本权限

清理示例:

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