一、Shell脚本开发
Shell脚本是Hook系统中最常用的脚本类型,适用于Unix/Linux和macOS环境。Bash和Zsh作为主流的Shell解释器,提供了丰富的脚本编程能力,能够高效地处理文件操作、文本处理和命令调用等任务。
1.1 Bash脚本基础:读取参数和环境变量
Hook脚本通过命令行参数和环境变量获取上下文信息。Shell脚本中使用 $1, $2, $@ 等特殊变量读取位置参数,使用 $VARIABLE_NAME 语法读取环境变量。
#!/bin/bash
# Hook脚本入口模板
# $1 - 第一个参数(通常是事件数据文件路径)
# $2 - 第二个参数(通常是输出文件路径)
EVENT_TYPE="${EVENT_TYPE:-unknown}"
echo "当前事件类型: $EVENT_TYPE"
# 读取参数
DATA_FILE="$1"
if [ -z "$DATA_FILE" ]; then
echo "错误: 未指定数据文件路径" >&2
exit 1
fi
echo "数据文件: $DATA_FILE"
最佳实践: 始终为环境变量提供默认值(使用 ${VAR:-default} 语法),避免变量未定义导致的脚本错误。
1.2 条件判断和退出码返回
Shell脚本通过 if/case 语句进行条件判断,使用 exit N 返回退出码。Hook系统根据脚本的退出码决定是否继续执行后续操作。
# 条件判断:检查命令是否执行成功
if command -v jq >/dev/null 2>&1; then
echo "jq 已安装,使用 jq 解析 JSON"
TITLE=$(jq -r '.title // empty' "$DATA_FILE")
else
echo "jq 未安装,使用 grep/sed 解析" >&2
TITLE=$(grep -o '"title":"[^"]*"' "$DATA_FILE" | cut -d'"' -f4)
fi
# 退出码使用
# 0 - 成功(继续执行)
# 1 - 一般错误
# 2 - 严重错误
# 64 - 临时性错误(可重试)
if [ -z "$TITLE" ]; then
echo "错误: 未能提取标题" >&2
exit 1
fi
exit 0
重要: 在 before Hook 中,非零退出码会阻断事件执行。请确保在需要放行操作的before Hook中返回 exit 0。
1.3 脚本路径解析
Hook脚本需要知道自身所在目录,以便引用同目录下的配置文件或辅助脚本。以下是获取脚本所在目录的可靠方法:
# 获取脚本所在目录(兼容各种调用方式)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "脚本目录: $SCRIPT_DIR"
# 加载同目录下的配置文件
CONFIG_FILE="$SCRIPT_DIR/hook-config.sh"
if [ -f "$CONFIG_FILE" ]; then
source "$CONFIG_FILE"
fi
1.4 常用Shell命令组合技巧
| 命令组合 |
用途 |
示例 |
管道 | |
将前一个命令的输出作为后一个命令的输入 |
cat file | grep "pattern" |
重定向 > >> |
将输出写入文件(覆盖/追加) |
echo "log" >> /tmp/hook.log |
子命令 $() |
将命令输出作为变量值或参数 |
NOW=$(date +%s) |
条件执行 && || |
根据前一个命令的成功/失败执行后续操作 |
mkdir -p dir && cd dir || exit 1 |
进程替换 <() |
将命令输出当作文件传递 |
diff <(cmd1) <(cmd2) |
1.5 错误处理(set -e / trap)
#!/bin/bash
# 健壮的Shell Hook脚本模板
set -e # 遇到错误立即退出
set -u # 使用未定义变量时报错
set -o pipefail # 管道中任一命令失败即返回非零
# 定义清理函数
cleanup() {
local exit_code=$?
echo "[HOOK] 清理中,退出码: $exit_code" >&2
rm -f /tmp/hook_temp_* 2>/dev/null || true
exit $exit_code
}
# 注册退出陷阱
trap cleanup EXIT
trap 'echo "中断信号接收,退出"; exit 130' INT TERM
# 主逻辑
main() {
echo "Hook脚本开始执行"
# ... 业务逻辑 ...
echo "Hook脚本执行完毕"
}
main "$@"
核心要点: Shell脚本开发中,参数读取($1, $@)、退出码控制(exit 0/1)、路径解析($(dirname ...))和错误处理(set -e, trap)是构建健壮Hook的四大基石。
二、Python脚本开发
Python脚本在Hook开发中具有跨平台、库丰富、易维护等优势。相比Shell脚本,Python能更好地处理复杂数据结构和逻辑,同时在Windows环境下也有良好支持。
2.1 读取环境变量(os.environ)
# === 文件: .claude/hooks/my_hook.py ===
# 将此文件保存为 .py,然后在 settings.json 中通过 python3 调用
import os
import sys
import json
# 读取环境变量(使用 .get() 安全读取,不存在时返回默认值)
event_type = os.environ.get('EVENT_TYPE', 'unknown')
tool_name = os.environ.get('TOOL_NAME', '')
hook_name = os.environ.get('HOOK_NAME', '')
status = os.environ.get('STATUS', '')
output_file = os.environ.get('OUTPUT_FILE', '')
print(f"事件类型: {event_type}")
print(f"工具名称: {tool_name}")
print(f"Hook名称: {hook_name}")
// settings.json 中调用 Python 脚本的正确方式
{
"hooks": {
"Stop": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "python3 .claude/hooks/my_hook.py"
}]
}]
}
}
提示: os.environ.get('KEY', 'default') 是安全的读取方式,即使环境变量不存在也不会抛出异常,而是返回默认值。注意:Python 脚本不能直接写在 Hook 的 command 字段中——必须保存为独立的 .py 文件,然后通过 python3 文件路径 调用。command 字段本质上是 Shell 命令,Shell 无法解析 import、os.environ 等 Python 语法。
2.2 处理事件数据(JSON解析)
def load_event_data(data_file: str) -> dict:
"""加载并解析事件数据JSON文件"""
try:
with open(data_file, 'r', encoding='utf-8') as f:
return json.load(f)
except FileNotFoundError:
print(f"错误: 数据文件未找到 {data_file}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"错误: JSON解析失败 - {e}", file=sys.stderr)
sys.exit(1)
def process_event(data: dict) -> int:
"""处理事件数据并返回退出码"""
event = data.get('event', {})
event_type = event.get('type', 'unknown')
if event_type == 'tool_use':
tool_name = event.get('tool', '')
arguments = event.get('arguments', {})
print(f"工具调用: {tool_name}")
print(f"参数: {json.dumps(arguments, indent=2)}")
return 0
elif event_type == 'message':
print(f"消息内容: {event.get('content', '')}")
return 0
else:
print(f"未知事件类型: {event_type}", file=sys.stderr)
return 1
if __name__ == '__main__':
# 第一个参数是事件数据文件路径
if len(sys.argv) < 2:
print("用法: hook.py <data_file> [output_file]", file=sys.stderr)
sys.exit(1)
data = load_event_data(sys.argv[1])
exit_code = process_event(data)
sys.exit(exit_code)
2.3 Python脚本的跨平台优势
- 统一API: 同一套代码可在Windows、macOS、Linux上运行,无需修改
- 路径处理: 使用
pathlib 模块自动处理平台路径分隔符差异
- 编码处理: 内置Unicode支持,避免Shell脚本中的编码问题
- 错误追溯: 异常堆栈信息清晰,便于调试
2.4 常用Python库集成
import os
import sys
import json
import subprocess
from pathlib import Path
# requests 库(需安装)
try:
import requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
def run_shell_command(cmd: list) -> subprocess.CompletedProcess:
"""执行Shell命令并处理结果"""
result = subprocess.run(
cmd,
capture_output=True,
text=True,
encoding='utf-8',
timeout=30
)
if result.returncode != 0:
print(f"命令失败 (exit={result.returncode}):", file=sys.stderr)
print(result.stderr, file=sys.stderr)
return result
def notify_webhook(url: str, payload: dict) -> bool:
"""发送Webhook通知"""
if not HAS_REQUESTS:
print("requests 库未安装,跳过通知", file=sys.stderr)
return False
try:
resp = requests.post(url, json=payload, timeout=10)
resp.raise_for_status()
return True
except Exception as e:
print(f"通知发送失败: {e}", file=sys.stderr)
return False
json
标准库,用于解析和生成JSON格式的事件数据
subprocess
标准库,执行外部命令并捕获输出
pathlib
标准库,跨平台路径操作,替代 os.path
requests
第三方库,HTTP请求,适合Webhook通知
核心要点: Python脚本开发的核心是 os.environ(读取环境变量)、json.load(解析事件数据)和 sys.exit(返回退出码)。相比Shell脚本,Python在复杂逻辑处理、错误追溯和跨平台兼容性方面具有明显优势。
三、输入输出处理
Hook脚本的输入输出处理是与其他系统和用户交互的关键。正确使用标准流和退出码可以确保Hook系统的稳定运行。
3.1 stdout:正常输出信息
标准输出(stdout, 文件描述符1)用于输出正常结果信息。Hook系统通常会捕获stdout并记录到日志中。
# === Shell 方式 —— stdout 标准输出 ===
echo "开始处理数据..."
echo "文件大小: $(stat -f%z "$file") bytes"
# === Python 方式 —— stdout 标准输出 ===
import sys
print("开始处理数据...")
print(f"文件大小: {file_size} bytes")
3.2 stderr:错误和调试信息
标准错误输出(stderr, 文件描述符2)用于输出错误和调试信息。将错误信息与正常输出分离,便于问题定位。
# === Shell 方式 —— 重定向到 stderr ===
echo "错误: 文件不存在" >&2
echo "[DEBUG] 当前目录: $(pwd)" >&2
# === Python 方式 —— 使用 file 参数输出到 stderr ===
import sys
print("错误: 文件不存在", file=sys.stderr)
print(f"[DEBUG] 当前目录: {os.getcwd()}", file=sys.stderr)
3.3 退出码对照表
| 退出码 |
含义 |
Hook系统行为 |
| 0 |
成功 |
继续执行后续Hook(或放行事件) |
| 1 |
一般错误 |
记录错误日志,根据配置决定是否阻断 |
| 2 |
严重错误 |
阻断事件执行(before Hook)或标记失败 |
| 64 |
临时性错误 |
触发重试机制(如果配置了重试策略) |
| 126 |
权限错误 |
脚本不可执行,检查文件权限 |
| 127 |
命令未找到 |
解释器或依赖命令不存在 |
| 130 |
用户中断 |
脚本被Ctrl+C中断 |
| 255 |
退出码越界 |
退出码模256后的结果 |
3.4 before Hook的非零退出码可阻断事件
关键机制: 在 before 类型Hook中,任何非零退出码都会阻止事件继续传递。这使你可以用Hook实现安全审计、格式验证、权限检查等功能。在 after 类型Hook中,非零退出码不影响事件本身,但会被记录到日志。
#!/bin/bash
# before Hook示例:权限检查
REQUIRED_ROLE="${HOOK_REQUIRED_ROLE:-admin}"
USER_ROLE="${USER_ROLE:-guest}"
echo "[权限检查] 所需角色: $REQUIRED_ROLE, 当前角色: $USER_ROLE"
case "$USER_ROLE" in
"$REQUIRED_ROLE"|superadmin)
echo "权限验证通过"
exit 0 # 放行
;;
*)
echo "权限不足! 需要 $REQUIRED_ROLE 角色" >&2
exit 1 # 阻断事件
;;
esac
核心要点: stdout输出正常信息,stderr输出错误和调试信息。退出码 0 表示成功,非 0 表示失败。before Hook中返回非零退出码可阻断事件执行,是一个重要的安全和控制机制。
四、环境变量使用指南
Hook系统通过环境变量向脚本传递丰富的上下文信息。熟练掌握这些环境变量是编写高效Hook脚本的基础。
4.1 常用环境变量一览
| 变量名 |
说明 |
示例值 |
$EVENT_TYPE |
当前触发的事件类型 |
tool_use, message |
$TOOL_NAME |
当前调用的工具名称 |
Bash, Read |
$HOOK_NAME |
当前执行的Hook名称 |
before_bash, after_read |
$STATUS |
事件处理状态 |
before, after, error |
$OUTPUT_FILE |
输出文件路径 |
/tmp/hook_output_xxx.json |
$DATA_FILE |
事件数据文件路径 |
/tmp/hook_data_xxx.json |
$HOOK_DIR |
Hook脚本所在目录 |
/home/user/.claude/hooks/ |
$PROJECT_ROOT |
项目根目录 |
/home/user/my-project/ |
4.2 读取环境变量的Shell和Python方法
Shell 读取
$VAR_NAME 直接读取
${VAR_NAME:-default} 带默认值
${VAR_NAME:?错误信息} 为空时报错退出
Python 读取
os.environ['KEY'] 直接读取(KeyError)
os.environ.get('KEY') 安全读取(None)
os.environ.get('KEY', 'default') 带默认值
#!/usr/bin/env python3
"""环境变量综合使用示例"""
import os
import sys
# 安全读取所有Hook环境变量
hook_env = {
'EVENT_TYPE': os.environ.get('EVENT_TYPE', ''),
'TOOL_NAME': os.environ.get('TOOL_NAME', ''),
'HOOK_NAME': os.environ.get('HOOK_NAME', ''),
'STATUS': os.environ.get('STATUS', ''),
'OUTPUT_FILE': os.environ.get('OUTPUT_FILE', ''),
'DATA_FILE': os.environ.get('DATA_FILE', ''),
'HOOK_DIR': os.environ.get('HOOK_DIR', ''),
'PROJECT_ROOT': os.environ.get('PROJECT_ROOT', ''),
}
# 输出当前环境上下文
print("=== Hook 环境变量 ===")
for key, value in hook_env.items():
if value:
print(f" {key}={value}")
# 根据事件类型执行不同逻辑
event_type = hook_env['EVENT_TYPE']
if event_type == 'tool_use':
tool_name = hook_env['TOOL_NAME']
before_commands = {
'Bash': lambda: print(f"[Bash Hook] 检查命令安全性..."),
'Read': lambda: print(f"[Read Hook] 验证文件访问权限..."),
'Write': lambda: print(f"[Write Hook] 检查写入路径是否合法..."),
}
handler = before_commands.get(tool_name)
if handler:
handler()
else:
print(f"[{tool_name}] 未注册处理函数,放行")
sys.exit(0)
4.3 自定义环境变量传递
提示: 在Hook配置中,可以通过 env 字段传递自定义环境变量。这些变量会与系统提供的环境变量合并,一起传递给Hook脚本。自定义变量使用专有前缀(如 HOOK_ 或 HOOK_CUSTOM_)避免与系统变量冲突。
# === Shell 方式 —— 读取自定义环境变量 ===
CUSTOM_VALUE="${HOOK_CUSTOM_VALUE:-}"
echo "自定义变量: ${CUSTOM_VALUE:-未设置}"
# === Python 方式 —— 读取自定义环境变量 ===
import os
custom_value = os.environ.get('HOOK_CUSTOM_VALUE', '未设置')
print(f"自定义变量: {custom_value}")
核心要点: $EVENT_TYPE、$TOOL_NAME、$HOOK_NAME、$STATUS、$OUTPUT_FILE 是最常用的五个Hook环境变量。Shell中通过 $VAR 读取并推荐使用 ${VAR:-default} 默认值语法;Python中通过 os.environ.get('VAR') 安全读取。
五、脚本调试技巧
Hook脚本在自动化环境中运行,调试方式与传统交互式脚本有所不同。掌握正确的调试技巧可以大幅提高开发效率。
5.1 echo/print 调试输出
# === Shell 方式 —— 带标记的调试输出 ===
echo "[HOOK_DEBUG] 事件类型: $EVENT_TYPE" >&2
echo "[HOOK_DEBUG] 当前目录: $(pwd)" >&2
echo "[HOOK_DEBUG] 参数数量: $#" >&2
# === Python 方式 —— 使用 f-string 格式化输出 ===
import sys
import os
event_type = os.environ.get('EVENT_TYPE', 'unknown')
print(f"[HOOK_DEBUG] 事件类型: {event_type}", file=sys.stderr)
print(f"[HOOK_DEBUG] 当前目录: {os.getcwd()}", file=sys.stderr)
print(f"[HOOK_DEBUG] 参数数量: {len(sys.argv)}", file=sys.stderr)
调试技巧: 使用 [HOOK_DEBUG] 前缀标记调试信息,并在正式版本中通过环境变量 HOOK_DEBUG=true 控制是否输出。将调试信息写入stderr可以避免干扰stdout的正常输出流。
5.2 日志文件写入
#!/bin/bash
# Shell日志文件示例
LOG_FILE="/tmp/hook-${HOOK_NAME:-unknown}.log"
log() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
}
log "INFO" "Hook脚本启动"
log "INFO" "事件类型: $EVENT_TYPE"
log "DEBUG" "工具名称: $TOOL_NAME"
log "ERROR" "处理失败: 文件未找到" # 仅在出错时使用
echo "日志已写入: $LOG_FILE"
#!/usr/bin/env python3
"""Python日志文件示例"""
import os
import sys
import logging
from logging.handlers import RotatingFileHandler
def setup_logger(hook_name: str) -> logging.Logger:
"""配置带轮转的日志记录器"""
log_dir = os.environ.get('HOOK_LOG_DIR', '/tmp/hook-logs')
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, f"{hook_name}.log")
logger = logging.getLogger(hook_name)
logger.setLevel(logging.DEBUG)
# 文件处理器(轮转,最大5MB,保留3个备份)
fh = RotatingFileHandler(
log_file, maxBytes=5*1024*1024, backupCount=3,
encoding='utf-8'
)
fh.setLevel(logging.DEBUG)
formatter = logging.Formatter(
'%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
fh.setFormatter(formatter)
logger.addHandler(fh)
# 控制台处理器(仅在DEBUG模式下输出到stderr)
if os.environ.get('HOOK_DEBUG'):
ch = logging.StreamHandler(sys.stderr)
ch.setLevel(logging.DEBUG)
ch.setFormatter(formatter)
logger.addHandler(ch)
return logger
logger = setup_logger(os.environ.get('HOOK_NAME', 'default'))
logger.info("Hook脚本启动")
logger.debug(f"事件类型: {os.environ.get('EVENT_TYPE')}")
logger.error("处理失败")
5.3 临时文件辅助调试
#!/bin/bash
# 使用临时文件保存中间数据
TEMP_DIR="/tmp/hook-debug"
mkdir -p "$TEMP_DIR"
# 保存事件数据到临时文件
cp "$DATA_FILE" "$TEMP_DIR/event_data.json"
# 保存环境变量快照
env | sort > "$TEMP_DIR/env_snapshot.txt"
# 保存脚本输出
exec 1>"$TEMP_DIR/output.log"
exec 2>"$TEMP_DIR/error.log"
echo "调试数据已保存到: $TEMP_DIR"
echo "查看环境变量: cat $TEMP_DIR/env_snapshot.txt"
echo "查看输出: cat $TEMP_DIR/output.log"
5.4 逐步调试复杂Hook
调试方法论:
- 分离测试: 将Hook逻辑拆分为独立函数,分别测试每个函数
- 输入验证: 确认事件数据JSON结构符合预期,使用
jq . 格式化查看
- 最小化复现: 创建最小的事件数据文件,反复触发Hook测试
- 退出码追踪: Hook执行完后检查退出码:
echo $?
- 模拟演练: 使用
bash -x hook.sh 或 python -m trace hook.py 追踪执行过程
#!/bin/bash
# 启用bash调试模式
# bash -x hook.sh # 命令行执行时加 -x 参数
# 或者在脚本内局部启用
set -x # 开启调试输出
# 需要调试的代码块
echo "当前事件: $EVENT_TYPE"
echo "数据文件内容:"
head -20 "$DATA_FILE"
set +x # 关闭调试输出
echo "后续代码正常执行..."
# 快速检查退出码
python3 -c "
import sys, json
data = json.load(open('$DATA_FILE'))
print('事件类型:', data.get('event', {}).get('type'))
sys.exit(0 if data.get('event') else 1)
"
echo "Python检查退出码: $?"
注意事项:
- 生产环境中避免输出过多调试信息,通过
HOOK_DEBUG 环境变量控制开关
- 临时调试文件应及时清理,避免磁盘空间占用
- 日志轮转配置(如Python的
RotatingFileHandler)可以防止日志文件无限增长
- 调试完成后移除
set -x 等调试代码
核心要点: 调试输出通过stderr分离(避免干扰stdout),使用日志文件记录详细执行过程,临时文件保存中间状态便于分析。复杂Hook应遵循"分步测试-验证输入-追踪退出码"的调试流程。善用 bash -x 和 HOOK_DEBUG 环境变量控制调试粒度。