本文档提供实际可用的Hook脚本示例,可直接集成到项目中。
文件: hooks/security-gate.sh
#!/bin/bash
set -euo pipefail
# 从stdin读取JSON输入
read INPUT
# 提取工具名称
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty')
if [[ -z "$TOOL" ]]; then
# 如果无法解析,允许操作
echo '{"decision":"allow","systemMessage":"Warning: Could not parse tool name"}' >&2
exit 0
fi
# 黑名单:禁止危险操作
case "$TOOL" in
"delete_file"|"remove_directory"|"shell")
# 这些操作被禁止
echo '{
"decision": "deny",
"reason": "Tool '"$TOOL"' is blocked by security policy"
}'
exit 0
;;
esac
# 允许其他操作
echo '{"decision":"allow"}'
exit 0配置:
{
"hooks": {
"BeforeTool": [
{
"hooks": [
{
"type": "command",
"command": "bash ./hooks/security-gate.sh",
"timeout": 5000
}
]
}
]
}
}文件: hooks/enhance-prompt.sh
#!/bin/bash
set -euo pipefail
read INPUT
# 添加系统指导
SYSTEM_INSTRUCTION="[SYSTEM GUIDELINES]
- Always verify operations before proceeding
- Never modify system-critical files
- Log all important actions
- Ask for confirmation for dangerous operations
- Provide explanations for all recommendations
[END GUIDELINES]"
echo "{
\"decision\": \"allow\",
\"hookSpecificOutput\": {
\"hookEventName\": \"BeforeAgent\",
\"additionalContext\": \"$SYSTEM_INSTRUCTION\"
}
}"
exit 0配置:
{
"hooks": {
"BeforeAgent": [
{
"hooks": [
{
"type": "command",
"command": "bash ./hooks/enhance-prompt.sh",
"timeout": 2000
}
]
}
]
}
}文件: hooks/audit-log.sh
#!/bin/bash
set -euo pipefail
read INPUT
TOOL=$(echo "$INPUT" | jq -r '.tool_name // "unknown"')
TIMESTAMP=$(echo "$INPUT" | jq -r '.timestamp')
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
# 写入审计日志
LOG_FILE="logs/audit-$(date +%Y%m%d).log"
mkdir -p logs
# 记录到文件(不要让失败阻止操作)
echo "[$TIMESTAMP] AUDIT: Tool=$TOOL Session=$SESSION_ID" >> "$LOG_FILE" 2>/dev/null || true
# 总是允许
echo '{"decision":"allow"}'
exit 0配置:
{
"hooks": {
"AfterTool": [
{
"hooks": [
{
"type": "command",
"command": "bash ./hooks/audit-log.sh"
}
]
}
]
}
}文件: hooks/role-based-access.sh
#!/bin/bash
set -euo pipefail
read INPUT
# 从环境变量或配置获取用户角色
USER_ROLE="${USER_ROLE:-viewer}"
case "$USER_ROLE" in
"admin")
# 管理员:允许所有工具
echo '{"decision":"allow"}'
;;
"developer")
# 开发者:禁止删除操作
echo '{
"hookSpecificOutput": {
"toolConfig": {
"mode": "ANY",
"allowedFunctionNames": [
"read_file",
"list_directory",
"write_file",
"edit_file",
"web_fetch",
"web_search",
"shell"
]
}
}
}'
;;
"viewer")
# 查看者:仅读取权限
echo '{
"hookSpecificOutput": {
"toolConfig": {
"mode": "ANY",
"allowedFunctionNames": [
"read_file",
"list_directory",
"web_fetch",
"web_search"
]
}
}
}'
;;
*)
# 未知角色:拒绝所有
echo '{
"decision": "deny",
"reason": "Unknown user role: '$USER_ROLE'"
}'
;;
esac
exit 0配置:
{
"hooks": {
"BeforeToolSelection": [
{
"hooks": [
{
"type": "command",
"command": "bash ./hooks/role-based-access.sh",
"timeout": 3000
}
]
}
]
}
}文件: hooks/tune-llm-params.sh
#!/bin/bash
set -euo pipefail
read INPUT
# 获取第一条消息内容
FIRST_MESSAGE=$(echo "$INPUT" | jq -r '.llm_request.messages[0].content // ""')
MSG_LENGTH=${#FIRST_MESSAGE}
# 根据内容长度调整温度
if [[ $MSG_LENGTH -gt 1000 ]]; then
# 长提示:复杂问题,增加创意
TEMPERATURE="0.8"
MAX_TOKENS="4096"
elif [[ $MSG_LENGTH -gt 500 ]]; then
# 中等长度
TEMPERATURE="0.7"
MAX_TOKENS="2048"
else
# 短提示:简单问题,降低创意
TEMPERATURE="0.3"
MAX_TOKENS="1024"
fi
# 检查是否涉及代码
if echo "$FIRST_MESSAGE" | grep -qiE "(code|script|function|class|def|import|export)"; then
# 代码问题:精确性更重要
TEMPERATURE="0.1"
MAX_TOKENS="4096"
fi
echo "{
\"hookSpecificOutput\": {
\"llm_request\": {
\"config\": {
\"temperature\": $TEMPERATURE,
\"maxOutputTokens\": $MAX_TOKENS
}
}
}
}"
exit 0配置:
{
"hooks": {
"BeforeModel": [
{
"hooks": [
{
"type": "command",
"command": "bash ./hooks/tune-llm-params.sh",
"timeout": 2000
}
]
}
]
}
}文件: hooks/file-whitelist.sh
#!/bin/bash
set -euo pipefail
read INPUT
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.path // ""')
# 如果不是文件操作,允许
if [[ ! "$TOOL" =~ ^(read_file|write_file|edit_file|delete_file)$ ]]; then
echo '{"decision":"allow"}'
exit 0
fi
# 检查文件路径是否在允许的目录中
ALLOWED_DIRS=(
"/home/user/projects"
"/tmp"
"./src"
"./docs"
)
ALLOWED=false
for DIR in "${ALLOWED_DIRS[@]}"; do
if [[ "$FILE_PATH" == "$DIR"* ]]; then
ALLOWED=true
break
fi
done
if [[ "$ALLOWED" == "true" ]]; then
echo '{"decision":"allow"}'
else
echo "{
\"decision\": \"deny\",
\"reason\": \"File path '$FILE_PATH' is not in allowed directories\"
}"
fi
exit 0配置:
{
"hooks": {
"BeforeTool": [
{
"matcher": "read_file|write_file|edit_file|delete_file",
"hooks": [
{
"type": "command",
"command": "bash ./hooks/file-whitelist.sh",
"timeout": 3000
}
]
}
]
}
}文件: hooks/session-tracker.sh
#!/bin/bash
set -euo pipefail
read INPUT
EVENT=$(echo "$INPUT" | jq -r '.hook_event_name')
SOURCE=$(echo "$INPUT" | jq -r '.source // .reason // "unknown"')
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
LOG_FILE="logs/sessions-$(date +%Y%m%d).log"
mkdir -p logs
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
# 记录会话事件
echo "[$TIMESTAMP] Event=$EVENT Source=$SOURCE SessionID=$SESSION_ID" >> "$LOG_FILE" 2>/dev/null || true
echo '{"decision":"allow"}'
exit 0配置:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "bash ./hooks/session-tracker.sh"
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "bash ./hooks/session-tracker.sh"
}
]
}
]
}
}将所有hooks集合到.gemini/settings.json:
{
"tools": {
"enableHooks": true
},
"hooks": {
"BeforeTool": [
{
"hooks": [
{
"type": "command",
"command": "bash ./hooks/security-gate.sh",
"timeout": 5000
}
]
},
{
"matcher": "read_file|write_file|edit_file|delete_file",
"hooks": [
{
"type": "command",
"command": "bash ./hooks/file-whitelist.sh",
"timeout": 3000
}
]
}
],
"AfterTool": [
{
"hooks": [
{
"type": "command",
"command": "bash ./hooks/audit-log.sh"
}
]
}
],
"BeforeAgent": [
{
"hooks": [
{
"type": "command",
"command": "bash ./hooks/enhance-prompt.sh",
"timeout": 2000
}
]
}
],
"BeforeModel": [
{
"hooks": [
{
"type": "command",
"command": "bash ./hooks/tune-llm-params.sh",
"timeout": 2000
}
]
}
],
"BeforeToolSelection": [
{
"hooks": [
{
"type": "command",
"command": "bash ./hooks/role-based-access.sh",
"timeout": 3000
}
]
}
],
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "bash ./hooks/session-tracker.sh"
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "bash ./hooks/session-tracker.sh"
}
]
}
]
}
}文件: hooks/test-hooks.sh
#!/bin/bash
# 测试hooks是否正确响应
test_hook() {
local hook_script=$1
local test_input=$2
local expected_decision=$3
echo "Testing $hook_script..."
RESULT=$(echo "$test_input" | bash "$hook_script" 2>/dev/null)
DECISION=$(echo "$RESULT" | jq -r '.decision // empty')
if [[ "$DECISION" == "$expected_decision" ]]; then
echo " ✓ PASS"
else
echo " ✗ FAIL: Expected '$expected_decision', got '$DECISION'"
echo " Output: $RESULT"
fi
}
# 测试安全网关
TEST_INPUT='{
"session_id": "test",
"cwd": "/tmp",
"hook_event_name": "BeforeTool",
"timestamp": "2025-01-15T10:00:00Z",
"tool_name": "delete_file",
"tool_input": {"path": "/tmp/test"}
}'
test_hook "./hooks/security-gate.sh" "$TEST_INPUT" "deny"
# 测试允许工具
TEST_INPUT='{
"session_id": "test",
"cwd": "/tmp",
"hook_event_name": "BeforeTool",
"timestamp": "2025-01-15T10:00:00Z",
"tool_name": "read_file",
"tool_input": {"path": "/tmp/test"}
}'
test_hook "./hooks/security-gate.sh" "$TEST_INPUT" "allow"bash hooks/test-hooks.sh在hook脚本中添加DEBUG变量:
#!/bin/bash
DEBUG=${DEBUG:-0}
read INPUT
if [[ $DEBUG -eq 1 ]]; then
echo "[DEBUG] Input: $INPUT" >&2
fi
# ... hook逻辑运行:
DEBUG=1 bash ./hooks/security-gate.sh < test_input.json- 序列执行:使用
sequential: true时,后续hook看到前一个的修改 - 并行执行:默认行为,所有hooks并发运行
- 超时:设置合理的超时避免长期阻塞(默认60秒)
- 轻量化:保持hooks简洁,复杂逻辑调用外部API
- 验证输入 - 检查JSON有效性
- 优雅降级 - Hook失败不应阻止系统
- 记录日志 - 用于审计和调试
- 版本控制 - Hooks代码应受版本控制
- 文档 - 记录每个hook的作用
- 测试 - 创建测试用例
- 监控 - 跟踪hook执行情况