主题
Gemini CLI 钩子:最佳实践
本指南涵盖在 Gemini CLI 中开发和部署钩子的安全考虑、性能优化、调试技术和隐私考虑。
安全考虑
验证所有输入
永远不要在没有验证的情况下信任来自钩子的数据。钩子输入可能包含恶意的用户提供数据:
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 [ -z "$tool_name" ]; then
echo "缺少 tool_name 字段" >&2
exit 1
fi使用超时
设置合理的超时以防止钩子无限期挂起:
json
{
"hooks": {
"BeforeTool": [
{
"matcher": "*",
"hooks": [
{
"name": "slow-validator",
"type": "command",
"command": "./hooks/validate.sh",
"timeout": 5000
}
]
}
]
}
}推荐超时:
- 快速验证:1000-5000ms
- 网络请求:10000-30000ms
- 重计算:30000-60000ms
限制权限
以最小所需权限运行钩子:
bash
#!/usr/bin/env bash
# 不要以 root 身份运行
if [ "$EUID" -eq 0 ]; then
echo "钩子不应以 root 身份运行" >&2
exit 1
fi
# 写入前检查文件权限
if [ -w "$file_path" ]; then
# 可以安全写入
else
echo "权限不足" >&2
exit 1
fi扫描密钥
使用 BeforeTool 钩子防止提交敏感数据:
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 访问密钥
/ghp_[a-zA-Z0-9]{36}/, // GitHub 个人访问令牌
/sk-[a-zA-Z0-9]{48}/, // OpenAI API 密钥
];
function containsSecret(content) {
return SECRET_PATTERNS.some((pattern) => pattern.test(content));
}审查外部脚本
在启用之前始终审查来自不受信任来源的钩子脚本:
bash
# 安装前审查
cat third-party-hook.sh | less
# 检查可疑模式
grep -E 'curl|wget|ssh|eval' third-party-hook.sh
# 验证钩子来源
ls -la third-party-hook.sh沙箱化不受信任的钩子
为了最大安全性,考虑在隔离环境中运行不受信任的钩子:
bash
# 在 Docker 容器中运行钩子
docker run --rm \
-v "$GEMINI_PROJECT_DIR:/workspace:ro" \
-i untrusted-hook-image \
/hook-script.sh < input.json性能
保持钩子快速
钩子同步运行——慢钩子会延迟代理循环。通过使用并行操作来优化速度:
javascript
// 顺序操作较慢
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());
// 优先使用并行操作以获得更好的性能
const [data1, data2, data3] = await Promise.all([
fetch(url1).then((r) => r.json()),
fetch(url2).then((r) => r.json()),
fetch(url3).then((r) => r.json()),
]);缓存昂贵操作
在调用之间存储结果以避免重复计算:
javascript
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}`; // 每小时缓存
if (cache[cacheKey]) {
console.log(JSON.stringify(cache[cacheKey]));
return;
}
// 昂贵操作
const result = await computeExpensiveResult();
cache[cacheKey] = result;
writeCache(cache);
console.log(JSON.stringify(result));
}使用适当的事件
选择与你的用例匹配的钩子事件以避免不必要的执行。AfterAgent 在每个代理循环完成时触发一次,而 AfterModel 在每次 LLM 调用后触发(每个循环可能多次):
json
// 如果检查最终完成,使用 AfterAgent 而不是 AfterModel
{
"hooks": {
"AfterAgent": [
{
"matcher": "*",
"hooks": [
{
"name": "final-checker",
"command": "./check-completion.sh"
}
]
}
]
}
}使用匹配器过滤
使用特定匹配器避免不必要的钩子执行。不要用 * 匹配所有工具,而是只指定你需要的工具:
json
{
"matcher": "write_file|replace",
"hooks": [
{
"name": "validate-writes",
"command": "./validate.sh"
}
]
}优化 JSON 解析
对于大输入,使用流式 JSON 解析器避免将所有内容加载到内存:
javascript
// 标准方法:解析整个输入
const input = JSON.parse(await readStdin());
const content = input.tool_input.content;
// 对于非常大的输入:流式处理并只提取需要的字段
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;
});调试
记录到文件
将调试信息写入专用日志文件:
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}..."
# 钩子逻辑在这里
log "钩子成功完成"使用 stderr 报告错误
stderr 上的错误消息根据退出码适当显示:
javascript
try {
const result = dangerousOperation();
console.log(JSON.stringify({ result }));
} catch (error) {
console.error(`钩子错误: ${error.message}`);
process.exit(2); // 阻塞错误
}独立测试钩子
使用示例 JSON 输入手动运行钩子脚本:
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 | .gemini/hooks/my-hook.sh
# 检查退出码
echo "退出码: $?"检查退出码
确保你的脚本返回正确的退出码:
bash
#!/usr/bin/env bash
set -e # 出错时退出
# 钩子逻辑
process_input() {
# ...
}
if process_input; then
echo "成功消息"
exit 0
else
echo "错误消息" >&2
exit 2
fi启用遥测
当 telemetry.logPrompts 启用时,钩子执行会被记录:
json
{
"telemetry": {
"logPrompts": true
}
}在日志中查看钩子遥测以调试执行问题。
使用钩子面板
/hooks panel 命令显示执行状态和最近输出:
bash
/hooks panel检查:
- 钩子执行计数
- 最近的成功/失败
- 错误消息
- 执行时间
开发
从简单开始
在实现复杂逻辑之前,从基本的日志钩子开始:
bash
#!/usr/bin/env bash
# 简单的日志钩子以了解输入结构
input=$(cat)
echo "$input" >> .gemini/hook-inputs.log
echo "已记录输入"使用 JSON 库
使用适当的库解析 JSON,而不是文本处理:
不好:
bash
# 脆弱的文本解析
tool_name=$(echo "$input" | grep -oP '"tool_name":\s*"\K[^"]+')好:
bash
# 健壮的 JSON 解析
tool_name=$(echo "$input" | jq -r '.tool_name')使脚本可执行
始终使钩子脚本可执行:
bash
chmod +x .gemini/hooks/*.sh
chmod +x .gemini/hooks/*.js版本控制
提交钩子以与团队共享:
bash
git add .gemini/hooks/
git add .gemini/settings.json
git commit -m "添加用于安全和测试的项目钩子".gitignore 考虑:
gitignore
# 忽略钩子缓存和日志
.gemini/hook-cache.json
.gemini/hook-debug.log
.gemini/memory/session-*.jsonl
# 保留钩子脚本
!.gemini/hooks/*.sh
!.gemini/hooks/*.js记录行为
添加描述以帮助他人理解你的钩子:
json
{
"hooks": {
"BeforeTool": [
{
"matcher": "write_file|replace",
"hooks": [
{
"name": "secret-scanner",
"type": "command",
"command": "$GEMINI_PROJECT_DIR/.gemini/hooks/block-secrets.sh",
"description": "在写入前扫描代码更改中的 API 密钥、密码和其他密钥"
}
]
}
]
}
}在钩子脚本中添加注释:
javascript
#!/usr/bin/env node
/**
* RAG 工具过滤钩子
*
* 此钩子通过从用户请求中提取关键词并基于语义相似性
* 过滤工具,将工具空间从 100+ 工具减少到约 15 个相关工具。
*
* 性能:平均约 500ms,缓存工具嵌入
* 依赖:@google/generative-ai
*/故障排除
钩子未执行
在 /hooks panel 中检查钩子名称:
bash
/hooks panel验证钩子出现在列表中并已启用。
验证匹配器模式:
bash
# 测试正则表达式模式
echo "write_file|replace" | grep -E "write_.*|replace"检查禁用列表:
json
{
"hooks": {
"disabled": ["my-hook-name"]
}
}确保脚本可执行:
bash
ls -la .gemini/hooks/my-hook.sh
chmod +x .gemini/hooks/my-hook.sh验证脚本路径:
bash
# 检查路径展开
echo "$GEMINI_PROJECT_DIR/.gemini/hooks/my-hook.sh"
# 验证文件存在
test -f "$GEMINI_PROJECT_DIR/.gemini/hooks/my-hook.sh" && echo "文件存在"钩子超时
检查配置的超时:
json
{
"name": "slow-hook",
"timeout": 60000
}优化慢操作:
javascript
// 之前:顺序操作(慢)
for (const item of items) {
await processItem(item);
}
// 之后:并行操作(快)
await Promise.all(items.map((item) => processItem(item)));使用缓存:
javascript
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;
}考虑拆分为多个更快的钩子:
json
{
"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 输出
输出前验证 JSON:
bash
#!/usr/bin/env bash
output='{"decision": "allow"}'
# 验证 JSON
if echo "$output" | jq empty 2>/dev/null; then
echo "$output"
else
echo "生成了无效的 JSON" >&2
exit 1
fi确保正确的引号和转义:
javascript
// 不好:未转义的字符串插值
const message = `用户说: ${userInput}`;
console.log(JSON.stringify({ message }));
// 好:自动转义
console.log(JSON.stringify({ message: `用户说: ${userInput}` }));检查二进制数据或控制字符:
javascript
function sanitizeForJSON(str) {
return str.replace(/[\x00-\x1F\x7F-\x9F]/g, ''); // 删除控制字符
}
const cleanContent = sanitizeForJSON(content);
console.log(JSON.stringify({ content: cleanContent }));退出码问题
验证脚本返回正确的代码:
bash
#!/usr/bin/env bash
set -e # 出错时退出
# 处理逻辑
if validate_input; then
echo "成功"
exit 0
else
echo "验证失败" >&2
exit 2
fi检查意外错误:
bash
#!/usr/bin/env bash
# 如果你想显式处理错误,不要使用 'set -e'
# set -e
if ! command_that_might_fail; then
# 处理错误
echo "命令失败但继续" >&2
fi
# 始终显式退出
exit 0使用 trap 进行清理:
bash
#!/usr/bin/env bash
cleanup() {
# 清理逻辑
rm -f /tmp/hook-temp-*
}
trap cleanup EXIT
# 钩子逻辑在这里环境变量不可用
检查变量是否设置:
bash
#!/usr/bin/env bash
if [ -z "$GEMINI_PROJECT_DIR" ]; then
echo "GEMINI_PROJECT_DIR 未设置" >&2
exit 1
fi
if [ -z "$CUSTOM_VAR" ]; then
echo "警告: CUSTOM_VAR 未设置,使用默认值" >&2
CUSTOM_VAR="default-value"
fi调试可用变量:
bash
#!/usr/bin/env bash
# 列出所有环境变量
env > .gemini/hook-env.log
# 检查特定变量
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:+<已设置>}" >> .gemini/hook-env.log使用 .env 文件:
bash
#!/usr/bin/env bash
# 如果存在则加载 .env 文件
if [ -f "$GEMINI_PROJECT_DIR/.env" ]; then
source "$GEMINI_PROJECT_DIR/.env"
fi隐私考虑
钩子输入和输出可能包含敏感信息。Gemini CLI 遵守钩子数据记录的 telemetry.logPrompts 设置。
收集哪些数据
钩子遥测可能包括:
- 钩子输入: 用户提示、工具参数、文件内容
- 钩子输出: 钩子响应、决策原因、添加的上下文
- 标准流: 钩子进程的 stdout 和 stderr
- 执行元数据: 钩子名称、事件类型、持续时间、成功/失败
隐私设置
启用(默认):
完整的钩子 I/O 记录到遥测。在以下情况下使用:
- 开发和调试钩子
- 遥测重定向到受信任的企业系统
- 你理解并接受隐私影响
禁用:
只记录元数据(事件名称、持续时间、成功/失败)。钩子输入和输出被排除。在以下情况下使用:
- 将遥测发送到第三方系统
- 处理敏感数据
- 隐私法规要求最小化数据收集
配置
在设置中禁用 PII 记录:
json
{
"telemetry": {
"logPrompts": false
}
}通过环境变量禁用:
bash
export GEMINI_TELEMETRY_LOG_PROMPTS=false钩子中的敏感数据
如果你的钩子处理敏感数据:
- 最小化日志: 不要将敏感数据写入日志文件
- 清理输出: 输出前删除敏感数据
- 使用安全存储: 加密静态敏感数据
- 限制访问: 限制钩子脚本权限
示例清理:
javascript
function sanitizeOutput(data) {
const sanitized = { ...data };
// 删除敏感字段
delete sanitized.apiKey;
delete sanitized.password;
// 编辑敏感字符串
if (sanitized.content) {
sanitized.content = sanitized.content.replace(
/api[_-]?key\s*[:=]\s*['"]?[a-zA-Z0-9_-]{20,}['"]?/gi,
'[已编辑]',
);
}
return sanitized;
}
console.log(JSON.stringify(sanitizeOutput(hookOutput)));