在Claude Code中使用Hook自动化工作流时,难免会遇到Hook执行失败、脚本报错或行为不符合预期的情况。掌握有效的调试方法和日志分析技巧,能够帮助开发者快速定位问题根源,减少排查时间,提升开发效率。本文系统性地总结了Hook调试的完整方法论,从基本原则到高级技巧,覆盖了日志查看、错误类型分析、模拟测试和逐步调试等核心环节。
适用场景: Hook脚本开发调试、CI/CD流水线中的Hook故障排查、权限和路径问题诊断、Hook超时和性能优化、多环境下的Hook行为验证
一、Hook调试的基本原则
理解Hook的执行环境是调试的基础。Claude Code的Hook系统在特定事件触发时执行用户定义的脚本,整个过程发生在受控的沙箱环境中。开发者需要明确Hook的执行上下文、触发条件和生命周期,才能有效地进行问题定位。
1.1 理解Hook的执行环境
Hook脚本运行在Claude Code宿主进程中,拥有与Claude Code相同的文件系统权限和环境变量。每个Hook都在独立的子进程中执行,这意味着Hook之间的状态不会共享,每次触发都是全新的一次性执行。理解这一点对于排查"为什么上一个Hook能工作而这次不行"这类问题至关重要。
# Hook执行环境的关键特征:
#
# 1. 独立进程:每个Hook调用在独立的子进程中运行
# 2. 环境继承:继承Claude Code进程的环境变量
# 3. 工作目录:通常是项目根目录($CLAUDE_CODE_PROJECT_DIR)
# 4. 标准流:stdin/stdout/stderr 连接到Claude Code日志系统
# 5. 超时限制:Hook有默认的执行超时时间(通常30秒)
# 6. 退出码:非零退出码表示Hook执行失败
# 快速检查Hook环境
echo "=== Hook调试信息 ==="
echo "PID: $$"
echo "工作目录: $(pwd)"
echo "Shell: $SHELL"
echo "用户: $(whoami)"
echo "PATH: $PATH"
echo "CLAUDE_CODE_PROJECT_DIR: ${CLAUDE_CODE_PROJECT_DIR:-未设置}"
1.2 检查Hook是否被触发
当Hook没有被触发时,现象往往是"静默失败"——没有任何输出,Claude Code正常工作但Hook仿佛不存在。此时的第一步应是确认Hook配置是否正确,以及触发事件是否匹配。可以通过以下方式验证:
# 方法一:在Hook脚本开头写入标记文件
# 在Hook脚本中添加以下内容:
touch /tmp/hook-triggered-$(date +%s).marker
# 执行后检查标记文件是否存在
ls -la /tmp/hook-triggered-*.marker
# 方法二:在Hook脚本中向日志文件写入时间戳
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Hook被触发:$CLAUDE_CODE_EVENT" >> /tmp/hook-debug.log
# 方法三:使用--verbose模式启动Claude Code
# claude --verbose
# 在verbose输出中搜索"Hook"或"hook"关键词
常见误区: 很多初学者以为Hook配置后就会自动触发所有事件。实际上,Hook只对特定事件类型生效。请检查 settings.json 中 hook 配置的 events 字段,确保事件类型匹配你的预期。
1.3 定位Hook执行的具体阶段
Hook的执行分为多个阶段,了解当前问题发生在哪个阶段可以大幅缩小排查范围:
# Hook执行生命周期
#
# 阶段一:配置加载
# - 读取 settings.json 中的hook配置
# - 验证Hook脚本路径是否存在
# - 检查Hook脚本是否具有执行权限
#
# 阶段二:事件触发
# - Claude Code产生特定事件(如 pre-message, post-tool-use)
# - 系统查找匹配的Hook配置
# - 准备环境变量和上下文数据
#
# 阶段三:脚本执行
# - 启动子进程执行Hook脚本
# - Hook脚本开始运行
# - 处理stdin输入(如有)
#
# 阶段四:结果收集
# - 等待Hook脚本执行完毕
# - 检查退出码
# - 收集stdout/stderr输出
# - 判断执行成功或失败
#
# 阶段五:后续处理
# - 成功:继续正常流程
# - 失败:执行错误处理逻辑(如重试、告警)
调试技巧: 在每个阶段的关键节点添加输出标记,可以帮助快速定位问题发生的阶段。例如在脚本开头输出 "STAGE: 开始执行",在逻辑分支处输出 "STAGE: 条件判断" 等。
二、Hook执行日志查看
日志是Hook调试最重要的信息来源。通过分析日志,可以还原Hook的执行过程,捕获错误信息,监控性能指标。Claude Code提供了多层次、多级别的日志系统,合理利用可以大幅加快故障排查速度。
2.1 Claude Code的日志文件位置
Claude Code的日志文件记录了完整的运行信息,包括Hook调用的相关输出。根据操作系统不同,日志文件位置有所差异:
# macOS / Linux 日志位置
~/.claude/logs/claude.log # 主日志文件
~/.claude/logs/hooks/ # Hook专用日志目录(如存在)
# Windows 日志位置
%USERPROFILE%\.claude\logs\claude.log # 主日志文件
# 使用以下命令实时查看日志(Linux/macOS)
tail -f ~/.claude/logs/claude.log | grep -i "hook"
# 查看最近的Hook相关日志
grep -i "hook" ~/.claude/logs/claude.log | tail -50
2.2 使用--verbose模式查看Hook执行详情
以verbose模式启动Claude Code可以输出更详细的运行信息,包括Hook的触发、执行和结果:
# 启动Claude Code的verbose模式
claude --verbose
# verbose模式下的Hook相关输出示例:
# [Hook] 加载配置文件: /path/to/project/.claude/settings.json
# [Hook] 注册事件处理器: pre-message
# [Hook] 事件触发: pre-message
# [Hook] 执行脚本: /path/to/project/.claude/hooks/pre-message.sh
# [Hook] 脚本输出: [调试信息] 开始处理消息...
# [Hook] 脚本退出码: 0
# [Hook] 执行成功 (用时: 0.342s)
# 也可以将verbose输出重定向到文件以便后续分析
claude --verbose 2>&1 | tee claude-verbose.log
推荐做法: 在开发Hook脚本阶段,始终使用 --verbose 模式运行Claude Code。这可以让你实时看到Hook的触发和输出,快速发现脚本中的错误。正式使用阶段可以关闭verbose模式以减少输出干扰。
2.3 Hook脚本中的日志输出
在Hook脚本中合理地输出日志信息,是自我诊断的重要手段。Claude Code会将Hook脚本的stdout和stderr输出捕获到日志系统中:
#!/bin/bash
# Hook脚本中的日志输出示例
# 定义日志函数,统一日志格式
log_info() {
echo "[INFO] [$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
log_warn() {
echo "[WARN] [$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2
}
log_error() {
echo "[ERROR] [$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2
}
log_debug() {
# 仅在 DEBUG 环境变量设置时输出调试信息
if [ "${DEBUG}" = "true" ]; then
echo "[DEBUG] [$(date '+%Y-%m-%d %H:%M:%S')] $*"
fi
}
# 使用示例
log_info "Hook开始执行"
log_debug "环境变量: CLAUDE_CODE_EVENT=${CLAUDE_CODE_EVENT}"
log_debug "工作目录: $(pwd)"
# 检查依赖
if ! command -v curl &>/dev/null; then
log_error "curl命令未找到,请安装curl"
exit 1
fi
log_info "curl可用,继续执行"
# 执行主要逻辑
log_info "正在处理..."
# 警告示例
if [ -z "${API_KEY}" ]; then
log_warn "API_KEY未设置,将使用默认配置"
fi
log_info "Hook执行完成"
2.4 日志级别(DEBUG/INFO/WARN/ERROR)
建立分级日志体系是专业Hook开发的标准实践。不同级别的日志服务于不同的目的和受众:
| 级别 | 用途 | 输出位置 | 适用场景 |
| DEBUG | 详细的调试信息,变量值、执行路径 | 条件输出(需设置DEBUG=true) | 开发阶段、问题复现 |
| INFO | 正常的执行流程信息 | stdout | 日常运行监控 |
| WARN | 潜在问题或非关键异常 | stderr | 配置缺失、降级处理 |
| ERROR | 导致Hook失败的错误 | stderr | 异常终止、需要人工介入 |
#!/bin/bash
# 带日志级别控制的Hook脚本模板
# 日志级别:0=ERROR, 1=WARN, 2=INFO, 3=DEBUG
LOG_LEVEL=${LOG_LEVEL:-2}
log() {
local level="$1"
local level_num="$2"
local message="$3"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
if [ "$level_num" -le "$LOG_LEVEL" ]; then
if [ "$level" = "ERROR" ]; then
echo "[${level}] [${timestamp}] ${message}" >&2
else
echo "[${level}] [${timestamp}] ${message}"
fi
fi
}
# 包装函数
log_debug() { log "DEBUG" 3 "$1"; }
log_info() { log "INFO" 2 "$1"; }
log_warn() { log "WARN" 1 "$1"; }
log_error() { log "ERROR" 0 "$1"; }
# 配置不同的日志级别来调试
# 正常运行时:LOG_LEVEL=2(显示INFO及以上)
# 调试时:LOG_LEVEL=3(显示所有日志)
# 静默模式:LOG_LEVEL=0(只显示ERROR)
最佳实践: 在开发调试时设置 `LOG_LEVEL=3` 或 `DEBUG=true` 以查看所有细节信息;在生产环境中设置 `LOG_LEVEL=1` 以减少日志输出量,同时保留关键的警告和错误信息。
三、常见Hook错误类型和解决
Hook执行失败的原因多种多样,但大多数可以归纳为几种常见的错误类型。理解这些错误类型的特征和排查方法,可以让故障定位更加高效。
退出码错误
Hook脚本返回非零退出码,最常见也最容易排查
3.1 退出码错误:非零退出码的分析
在Shell脚本中,退出码(exit code)是0表示成功,非零表示错误。Claude Code会检查Hook脚本的退出码,如果非零则判定Hook执行失败。不同的退出码代表不同的含义:
# 常见退出码对照表
#
# 退出码 含义 可能原因
# 0 成功 一切正常
# 1 通用错误 未知错误、参数错误
# 2 Shell内建命令错误 语法错误、命令未找到
# 126 命令不可执行 缺少执行权限
# 127 命令未找到 PATH配置错误、依赖缺失
# 128 无效退出参数 exit命令使用了无效参数
# 130 被Ctrl+C中断 手动终止了Hook执行
# 137 被SIGKILL信号杀死 超时被强制终止
# 139 段错误(SIGSEGV) 内存访问异常
# 255 退出码超出范围 exit -1 或类似
# 排查退出码的步骤:
# 1. 查看退出码数值:echo "退出码: $?"
# 2. 根据退出码定位问题类型
# 3. 检查脚本中对应的错误处理逻辑
# 4. 添加set -x跟踪执行过程
# 在脚本中显式设置退出码
if [ "$?" -ne 0 ]; then
log_error "上一个命令执行失败"
exit 1
fi
#!/bin/bash
# 退出码调试示例
set -euo pipefail # 严格模式:遇到错误立即退出
log_info "开始执行Hook..."
# 故意制造一个错误来演示退出码排查
if [ ! -f "/path/to/required/file" ]; then
log_error "必需的配置文件不存在"
exit 1
fi
# 使用trap捕获意外退出
trap 'log_error "Hook意外退出,退出码: $?"' EXIT
# 主要逻辑
do_something_important
# 清除trap
trap - EXIT
log_info "Hook执行成功"
注意: 使用 `set -e` 时,任何命令的失败都会导致脚本立即退出。这在某些场景下很有用,但也会导致非关键命令的失败变成致命错误。建议只在关键步骤使用条件判断,而不是全局开启 set -e。
3.2 超时:Hook执行超时的原因和解决
Claude Code对Hook执行有默认的超时限制(通常是30秒)。如果Hook脚本执行时间超过这个限制,进程会被强制终止。超时是生产环境中非常常见的Hook失败原因:
# 超时的常见原因:
#
# 1. 网络请求阻塞:curl请求远程API没有设置超时,等待响应时间过长
# 2. 无限循环:脚本中的循环逻辑缺少终止条件
# 3. 等待外部资源:等待文件锁、数据库连接、其他进程释放资源
# 4. 大文件处理:处理超大文件耗时过长
# 5. sleep时间过长:不必要的长时间sleep
# 解决方案一:在脚本内部设置超时
# 使用timeout命令包装长时间操作
timeout 10 curl -s https://api.example.com/data
if [ $? -eq 124 ]; then
log_error "curl请求超时(10秒)"
exit 1
fi
# 解决方案二:设置网络请求的超时参数
curl --connect-timeout 5 --max-time 15 "https://api.example.com"
# 解决方案三:使用并行处理的超时控制
# 在后台执行并设置超时
(
sleep 30
log_error "Hook执行超时"
kill $$ 2>/dev/null
) &
TIMER_PID=$!
# 主要逻辑
do_main_logic
# 取消定时器
kill $TIMER_PID 2>/dev/null
经验值: 大多数Hook脚本应该在1-3秒内完成执行。如果超过5秒,就应该考虑优化脚本逻辑或将耗时操作改为异步执行。网络请求务必设置连接超时(5秒)和总超时(15秒)。
3.3 路径错误:文件或目录不存在的排查
路径错误发生在Hook脚本引用的文件或目录不存在时。由于Hook的工作目录可能与预期不同,或者项目结构发生了变化,路径问题非常容易出现:
# 路径错误的常见形式:
#
# 1. 使用相对路径:工作目录不是预期的目录
# 2. 硬编码路径:路径写死在不同环境下不适用
# 3. 缺少前置检查:使用文件前未检查是否存在
# 4. 符号链接断裂:链接指向的目标已被移动或删除
# 解决方案一:始终使用绝对路径或基于项目根目录的路径
PROJECT_DIR="${CLAUDE_CODE_PROJECT_DIR:-$(pwd)}"
HOOK_DIR="${PROJECT_DIR}/.claude/hooks"
LOG_DIR="${PROJECT_DIR}/logs"
# 在访问路径前进行检查
if [ ! -d "$LOG_DIR" ]; then
log_warn "日志目录不存在,创建: ${LOG_DIR}"
mkdir -p "$LOG_DIR"
fi
# 解决方案二:将路径检查封装为函数
check_path() {
local path="$1"
local type="${2:-f}" # f=文件, d=目录
local label="${3:-路径}"
if [ "$type" = "f" ] && [ ! -f "$path" ]; then
log_error "${label}不存在 (文件): ${path}"
return 1
elif [ "$type" = "d" ] && [ ! -d "$path" ]; then
log_error "${label}不存在 (目录): ${path}"
return 1
fi
log_debug "${label}检查通过: ${path}"
return 0
}
# 使用示例
check_path "$PROJECT_DIR/package.json" "f" "项目配置文件" || exit 1
check_path "$HOOK_DIR" "d" "Hook目录" || exit 1
# 解决方案三:使用readlink获取真实路径
REAL_PATH=$(readlink -f "$SOME_PATH" 2>/dev/null || realpath "$SOME_PATH" 2>/dev/null)
if [ -z "$REAL_PATH" ]; then
log_error "无法解析路径: ${SOME_PATH}"
exit 1
fi
3.4 权限错误:脚本执行权限和文件访问权限
权限错误通常发生在Hook脚本没有执行权限,或者尝试访问没有权限的文件/目录时。在类Unix系统上,新创建的文件默认没有执行权限,这是一个非常容易被忽略的问题:
# 权限错误的常见场景:
#
# 场景一:脚本文件没有执行权限
# 错误信息:Permission denied
# 解决方案:
chmod +x /path/to/hook-script.sh
# 场景二:访问受保护的文件
# 错误信息:cat: /var/log/syslog: Permission denied
# 解决方案:使用sudo或以正确用户身份运行(不推荐在Hook中使用sudo)
# 或者将文件权限改为可读
chmod 644 /path/to/accessible-file
# 场景三:目录无写权限
# 错误信息:mkdir: cannot create directory: Permission denied
# 解决方案:选择有写权限的目录,或创建目录时使用sudo
# 权限检查工具函数
check_permissions() {
local path="$1"
local required="${2:-r}" # r=读, w=写, x=执行
if [ ! -e "$path" ]; then
log_error "路径不存在: ${path}"
return 1
fi
case "$required" in
r)
if [ ! -r "$path" ]; then
log_error "没有读取权限: ${path}"
return 1
fi
;;
w)
if [ ! -w "$path" ]; then
log_error "没有写入权限: ${path}"
return 1
fi
;;
x)
if [ ! -x "$path" ]; then
log_error "没有执行权限: ${path}"
return 1
fi
;;
esac
log_debug "权限检查通过: ${path} (${required})"
return 0
}
# 使用示例
check_permissions "/path/to/hook.sh" "x" || exit 1
check_permissions "/path/to/output.log" "w" || exit 1
3.5 环境变量缺失:必需变量未设置
Hook脚本依赖的环境变量如果没有正确设置,会导致脚本行为异常或执行失败。Claude Code会向Hook传递一组标准环境变量,但用户自定义变量需要额外配置:
# Claude Code传递给Hook的标准环境变量
#
# CLAUDE_CODE_EVENT 触发Hook的事件类型
# CLAUDE_CODE_PROJECT_DIR 项目根目录
# CLAUDE_CODE_SESSION_ID 当前会话ID
# CLAUDE_CODE_HOOK_DIR Hook脚本目录
# 环境变量缺失检查函数
check_env_vars() {
local missing=0
for var in "$@"; do
if [ -z "${!var:-}" ]; then
log_error "必需的环境变量未设置: ${var}"
missing=$((missing + 1))
else
log_debug "环境变量检查通过: ${var}=${!var}"
fi
done
if [ "$missing" -gt 0 ]; then
log_error "缺少 ${missing} 个必需的环境变量"
return 1
fi
return 0
}
# 使用示例
check_env_vars "CLAUDE_CODE_EVENT" "CLAUDE_CODE_PROJECT_DIR" "API_KEY" "WEBHOOK_URL" || exit 1
# 为环境变量设置默认值
API_KEY="${API_KEY:-}"
WEBHOOK_URL="${WEBHOOK_URL:-https://default-webhook.example.com}"
TIMEOUT="${TIMEOUT:-30}"
# 变量未设置时使用默认值(带警告)
if [ -z "${API_KEY}" ]; then
log_warn "API_KEY未设置,将使用空值"
fi
# 在settings.json中配置自定义环境变量
# .claude/settings.json
# {
# "env": {
# "API_KEY": "your-api-key",
# "WEBHOOK_URL": "https://hooks.example.com/notify"
# }
# }
最佳实践: 始终在Hook脚本开头执行环境变量验证,使用显式的检查并给出明确的错误信息。避免脚本在缺少关键配置时默默执行,这会导致更难排查的隐藏问题。在 settings.json 的 env 字段中集中管理所有自定义环境变量。
四、Hook模拟测试
在将Hook脚本部署到实际环境之前,使用模拟测试验证其正确性是一种非常有效的质量保障手段。模拟测试可以避免因Hook脚本错误而影响正常的Claude Code工作流程。
4.1 使用模拟事件测试Hook执行
模拟事件测试的核心思想是"假装自己是Claude Code",在命令行中手动触发Hook,传入与真实环境相同的参数和环境变量:
#!/bin/bash
# Hook模拟测试脚本
# 用法: ./test-hook.sh [event-type]
HOOK_SCRIPT="${1:-./.claude/hooks/pre-message.sh}"
EVENT_TYPE="${2:-pre-message}"
echo "============================================"
echo " Hook模拟测试"
echo "============================================"
echo "测试脚本: ${HOOK_SCRIPT}"
echo "模拟事件: ${EVENT_TYPE}"
echo ""
# 检查Hook脚本是否存在
if [ ! -f "$HOOK_SCRIPT" ]; then
echo "[ERROR] Hook脚本不存在: ${HOOK_SCRIPT}"
exit 1
fi
if [ ! -x "$HOOK_SCRIPT" ]; then
echo "[WARN] Hook脚本没有执行权限,尝试添加..."
chmod +x "$HOOK_SCRIPT"
fi
# 设置模拟环境变量
export CLAUDE_CODE_EVENT="${EVENT_TYPE}"
export CLAUDE_CODE_PROJECT_DIR="$(pwd)"
export CLAUDE_CODE_SESSION_ID="test-session-$(date +%s)"
export CLAUDE_CODE_HOOK_DIR="$(dirname "$HOOK_SCRIPT")"
# 可选:启用调试模式
export DEBUG=true
export LOG_LEVEL=3
echo "环境变量:"
echo " CLAUDE_CODE_EVENT=${CLAUDE_CODE_EVENT}"
echo " CLAUDE_CODE_PROJECT_DIR=${CLAUDE_CODE_PROJECT_DIR}"
echo " CLAUDE_CODE_SESSION_ID=${CLAUDE_CODE_SESSION_ID}"
echo " CLAUDE_CODE_HOOK_DIR=${CLAUDE_CODE_HOOK_DIR}"
echo ""
# 执行Hook脚本
echo "开始执行Hook脚本..."
echo "--------------------------------------------"
START_TIME=$(date +%s%N)
# 执行Hook脚本并捕获退出码
output=$("$HOOK_SCRIPT" 2>&1)
exit_code=$?
END_TIME=$(date +%s%N)
DURATION=$(( (END_TIME - START_TIME) / 1000000 ))
DURATION_SEC=$(echo "scale=3; ${DURATION} / 1000" | bc)
echo "--------------------------------------------"
echo ""
# 输出结果
echo "执行结果:"
echo " 退出码: ${exit_code}"
echo " 耗时: ${DURATION_SEC}秒"
echo " 输出行数: $(echo "$output" | wc -l)"
echo ""
if [ "$exit_code" -eq 0 ]; then
echo "[SUCCESS] Hook脚本测试通过"
else
echo "[FAILED] Hook脚本测试失败 (退出码: ${exit_code})"
fi
echo ""
echo "--- 完整输出 ---"
echo "$output"
echo "--- 输出结束 ---"
4.2 临时测试Hook脚本的正确性
在迭代开发Hook脚本时,频繁使用实际Claude Code操作来测试效率太低。使用临时测试脚本可以快速验证Hook逻辑的正确性:
#!/bin/bash
# 快速Hook测试(不依赖Claude Code环境)
# 保存为 test-quick.sh 并运行: bash test-quick.sh
# 模拟需要的环境变量
export WEBHOOK_URL="https://httpbin.org/post" # 测试用的HTTP回显服务
export CLAUDE_CODE_EVENT="post-tool-use"
# 模拟传递给Hook的stdin数据
echo '{"tool":"bash","args":{"command":"ls"}}' | \
bash .claude/hooks/post-tool-use.sh
echo "测试完成,退出码: $?"
# 使用临时目录测试文件操作
TEST_DIR=$(mktemp -d)
echo "使用临时目录: ${TEST_DIR}"
# 运行需要文件操作的Hook
# bash .claude/hooks/pre-message.sh
# 清理临时目录
rm -rf "$TEST_DIR"
echo "临时目录已清理"
推荐工具: 在Windows环境中,可以使用 httpbin.org 或 webhook.site 等在线服务来测试HTTP请求类的Hook;在Linux/macOS环境中,可以使用 nc(netcat)监听本地端口来验证网络请求的格式和内容。
4.3 设置模拟环境变量测试
Hook脚本的行为很大程度上依赖于环境变量。通过系统地设置不同的环境变量组合,可以全面测试Hook在各种条件下的行为:
#!/bin/bash
# 多场景环境变量测试
run_test() {
local test_name="$1"
shift
echo ""
echo "=== 测试场景: ${test_name} ==="
# 清除之前设置的环境变量
unset API_KEY WEBHOOK_URL DEBUG LOG_LEVEL
# 设置本场景的环境变量
while [ $# -gt 0 ]; do
export "$1"
shift
done
# 执行Hook脚本
bash .claude/hooks/notify.sh
echo "退出码: $?"
}
echo "========================================"
echo " Hook多场景测试"
echo "========================================"
# 场景一:正常情况
run_test "正常调用" \
"API_KEY=test-key-123" \
"WEBHOOK_URL=https://hooks.example.com" \
"DEBUG=false" \
"LOG_LEVEL=2"
# 场景二:缺少API_KEY
run_test "缺少API_KEY" \
"WEBHOOK_URL=https://hooks.example.com" \
"DEBUG=false" \
"LOG_LEVEL=2"
# 场景三:调试模式
run_test "调试模式" \
"API_KEY=test-key-123" \
"WEBHOOK_URL=https://hooks.example.com" \
"DEBUG=true" \
"LOG_LEVEL=3"
# 场景四:错误URL
run_test "无效Webhook URL" \
"API_KEY=test-key-123" \
"WEBHOOK_URL=https://invalid-url" \
"DEBUG=false" \
"LOG_LEVEL=2"
echo ""
echo "========================================"
echo " 全部测试完成"
echo "========================================"
4.4 测试Hook在不同条件下的行为
Hook脚本需要在各种边界条件下都能正常工作。以下是一个全面的测试清单,覆盖了Hook的主要风险点:
- 正常输入:标准输入数据格式正确
- 空输入:无输入数据或空字符串
- 特殊字符:输入包含空格、引号、特殊符号等
- 超大输入:输入数据超过100KB
- 网络超时:外部API响应缓慢
- 网络错误:外部API返回500或无法连接
- 并发执行:多个Hook同时触发
- 磁盘空间不足:写入文件时磁盘满
- 权限不足:文件只读或目录不可写
- 依赖缺失:必需的命令或工具不存在
#!/bin/bash
# 边界条件自动化测试
test_edge_case() {
local case_name="$1"
local setup_cmd="$2"
local expected_exit="$3"
echo -n "测试: ${case_name}... "
# 执行设置命令(准备测试条件)
eval "$setup_cmd" 2>/dev/null
# 运行Hook脚本
bash .claude/hooks/target-script.sh
actual_exit=$?
if [ "$actual_exit" -eq "$expected_exit" ]; then
echo "通过 (退出码: ${actual_exit})"
else
echo "失败 (预期: ${expected_exit}, 实际: ${actual_exit})"
fi
}
# 测试用例
test_edge_case "正常执行" "export VALID_INPUT=hello" 0
test_edge_case "空输入" "unset VALID_INPUT" 1
test_edge_case "特殊字符" "export VALID_INPUT='a\"b\\c'" 0
test_edge_case "超大输入" "export VALID_INPUT=$(python3 -c 'print(\"x\"*100000)')" 0
测试策略建议: 为每个Hook脚本创建一个独立的测试目录,包含测试脚本、模拟数据和预期结果。将测试纳入版本控制,每次修改Hook后运行测试套件,确保回归测试覆盖全面。
五、逐步调试方法
当Hook脚本的逻辑比较复杂,或者问题难以定位时,逐步调试是最有效的策略。通过在脚本中设置断点、分段执行、观察中间状态,可以精确地找到问题所在。
5.1 添加临时echo/print输出定位问题
最直接有效的调试方法是在Hook脚本的各个关键位置添加输出语句,打印变量值、执行状态和中间结果。这种方法虽然原始,但在大多数场景下是最快找到问题的方式:
#!/bin/bash
# 使用临时输出进行调试
echo ">>> [调试] 开始执行Hook"
echo ">>> [调试] 事件类型: ${CLAUDE_CODE_EVENT:-未知}"
echo ">>> [调试] 工作目录: $(pwd)"
# 检查输入
echo ">>> [调试] 检查输入数据..."
input_data=$(cat /dev/stdin 2>/dev/null || echo "")
echo ">>> [调试] 输入数据长度: ${#input_data}"
echo ">>> [调试] 输入数据前100字符: ${input_data:0:100}"
# 解析参数
param1="${1:-}"
param2="${2:-}"
echo ">>> [调试] 参数1: '${param1}'"
echo ">>> [调试] 参数2: '${param2}'"
# 关键步骤1:数据验证
if [ -z "$param1" ]; then
echo ">>> [调试] 参数1为空,需要回退到默认值"
param1="default"
fi
# 关键步骤2:外部调用
echo ">>> [调试] 准备调用外部API..."
echo ">>> [调试] curl命令: curl -s -X POST -H 'Content-Type: application/json' -d '{\"key\":\"${param1}\"}' https://api.example.com/endpoint"
response=$(curl -s -o /tmp/curl-response.txt -w "%{http_code}" \
-X POST \
-H "Content-Type: application/json" \
-d "{\"key\":\"${param1}\"}" \
https://api.example.com/endpoint 2>&1)
echo ">>> [调试] HTTP状态码: ${response}"
echo ">>> [调试] 响应内容: $(cat /tmp/curl-response.txt)"
# 关键步骤3:结果处理
if [ "$response" = "200" ]; then
echo ">>> [调试] API调用成功,继续处理..."
else
echo ">>> [调试] API调用失败,状态码: ${response}"
echo ">>> [调试] 将执行降级逻辑"
fi
echo ">>> [调试] Hook执行完毕"
# 调试完成后的清理
# 注意:调试输出会被Claude Code记录到日志中
提醒: 临时调试输出在问题定位后务必清理干净,避免生产环境中输出大量调试信息。可以在调试输出前加上特殊标记(如 ">>> [调试]"),方便后续使用grep全局搜索并移除。
5.2 使用exit提前退出测试
通过在不同的逻辑分支中插入提前退出的代码,可以快速验证某个特定分支是否被执行,以及执行的中间状态是否符合预期:
#!/bin/bash
# 使用提前退出进行分段调试
echo "=== 阶段一:环境检查 ==="
if [ -z "${REQUIRED_VAR}" ]; then
echo "错误:REQUIRED_VAR未设置"
exit 1 # 提前退出,验证环境检查逻辑
fi
echo "阶段一通过"
echo "=== 阶段二:输入处理 ==="
input=$(cat)
echo "输入内容: ${input}"
# exit 0 # 取消注释可在此处停止,查看输入内容
echo "=== 阶段三:数据处理 ==="
processed=$(echo "${input}" | tr 'a-z' 'A-Z')
echo "处理后: ${processed}"
# exit 0 # 取消注释可在此处停止,查看处理结果
echo "=== 阶段四:外部调用 ==="
# exit 0 # 取消注释可跳过外部调用,单独测试前三阶段
# 只有前面所有阶段都调试通过后,再取消注释继续
echo "所有阶段调试完成"
5.3 分步执行Hook逻辑
将Hook脚本拆分为多个独立的步骤或函数,逐步执行并验证每个步骤的结果,可以精确定位问题发生在哪个环节:
#!/bin/bash
# 分步调试的Hook脚本结构
# ========== 配置区 ==========
DEBUG_MODE="${DEBUG:-false}"
# ========== 步骤函数 ==========
step_1_validate_environment() {
echo "[步骤1/4] 验证运行环境..."
echo " 工作目录: $(pwd)"
echo " Shell版本: $(bash --version | head -1)"
echo " 必要命令检查..."
for cmd in curl jq git; do
if command -v "$cmd" &>/dev/null; then
echo " ✓ ${cmd} 可用"
else
echo " ✗ ${cmd} 不可用"
return 1
fi
done
echo " ✓ 环境验证通过"
return 0
}
step_2_prepare_data() {
echo "[步骤2/4] 准备数据..."
local data="${1:-}"
if [ -z "$data" ]; then
echo " 无输入数据,使用默认值"
data="default"
fi
echo " 数据准备完成: ${data}"
# 将数据保存到临时文件供后续步骤使用
echo "$data" > /tmp/hook-data.tmp
return 0
}
step_3_process_data() {
echo "[步骤3/4] 处理数据..."
if [ ! -f /tmp/hook-data.tmp ]; then
echo " 错误:数据文件不存在"
return 1
fi
local data=$(cat /tmp/hook-data.tmp)
echo " 处理数据: ${data}"
# 处理逻辑...
echo " 数据处理完成"
return 0
}
step_4_cleanup() {
echo "[步骤4/4] 清理临时文件..."
rm -f /tmp/hook-data.tmp
echo " ✓ 清理完成"
return 0
}
# ========== 主流程 ==========
echo "========================================="
echo " Hook逐步调试 - $(date)"
echo "========================================="
step_1_validate_environment
if [ $? -ne 0 ]; then
echo "[失败] 步骤1失败,终止执行"
exit 1
fi
step_2_prepare_data "$@"
if [ $? -ne 0 ]; then
echo "[失败] 步骤2失败,终止执行"
exit 2
fi
step_3_process_data
if [ $? -ne 0 ]; then
echo "[失败] 步骤3失败,终止执行"
exit 3
fi
step_4_cleanup
echo "========================================="
echo " Hook执行成功"
echo "========================================="
exit 0
分步调试优势: 每个步骤有明确的入口和出口,步骤之间通过临时文件或环境变量传递状态。当某一步失败时,可以清晰地知道失败在哪个环节,以及失败的具体原因。这种方法特别适合10行以上的复杂Hook脚本。
5.4 调试完成后的清理工作
调试完成后,需要进行全面的清理工作,确保生产环境中的Hook脚本干净、高效、安全:
# 调试完成后的清理检查清单
# 1. 移除临时调试输出
# 搜索并删除所有临时添加的echo/print调试语句
# 查找模式:grep -n ">>> \[调试\]" hook-script.sh
# 或使用:grep -n "echo \"===" hook-script.sh
# 2. 还原被注释的代码
# 检查是否有因为调试而临时注释掉的代码行
# grep -n "^# " hook-script.sh | grep -v "^#!"
# 3. 移除调试用的exit语句
# 搜索所有中途exit,确认是否应该保留
# grep -n "exit 0\|exit 1" hook-script.sh
# 4. 清理临时文件和缓存
# 检查脚本中是否有未清理的临时文件
# 确保使用trap在退出时清理:trap 'rm -f /tmp/hook-*' EXIT
# 5. 恢复日志级别
# 将LOG_LEVEL从3(DEBUG)恢复为2(INFO)或1(WARN)
# 将DEBUG=true改为DEBUG=false或删除
# 6. 最终验证
# 在非调试模式下运行一次,确认没有多余的调试输出
# DEBUG=false bash hook-script.sh 2>&1 | grep -v "^>>> \[调试\]"
#!/bin/bash
# 带完整trap清理的Hook脚本示例
# 创建临时目录用于调试/运行
TEMP_DIR=$(mktemp -d)
# 注册退出清理函数
cleanup() {
local exit_code=$?
echo "[INFO] 执行清理..."
# 删除临时目录
if [ -d "$TEMP_DIR" ]; then
rm -rf "$TEMP_DIR"
echo "[INFO] 临时目录已删除: ${TEMP_DIR}"
fi
# 恢复原始设置
if [ -n "${OLD_IFS:-}" ]; then
IFS="$OLD_IFS"
fi
echo "[INFO] 清理完成 (退出码: ${exit_code})"
}
# 注册trap,确保无论正常退出还是错误退出都执行清理
trap cleanup EXIT
trap 'echo "被信号中断"; exit 1' INT TERM
# ====== 主要逻辑 ======
echo "[INFO] Hook开始执行"
echo "[INFO] 临时目录: ${TEMP_DIR}"
# ... 主要逻辑 ...
echo "[INFO] Hook执行完成"
核心要点总结:
1. 理解Hook的执行环境(独立子进程、环境继承、事件驱动)是调试的基础
2. 日志是调试的核心手段,合理使用--verbose模式和分级日志(DEBUG/INFO/WARN/ERROR)
3. 常见错误可归纳为五类:退出码错误、超时、路径错误、权限错误、环境变量缺失
4. 模拟测试是提前发现问题的有效手段,应覆盖正常场景和边界条件
5. 逐步调试的核心方法:临时输出、提前退出、分步执行、trap清理
6. 调试完成后务必清理临时代码、恢复日志级别、移除调试输出
六、错误通知和告警配置
在Hook脚本中加入错误通知机制,可以在Hook执行失败时及时获得告警。结合前文介绍的日志系统和调试方法,形成一个完整的故障排查闭环。
#!/bin/bash
# 在Hook脚本中集成错误通知
# 通知函数:发送错误信息到指定渠道
send_alert() {
local subject="$1"
local message="$2"
local webhook_url="${ALERT_WEBHOOK_URL:-}"
if [ -z "$webhook_url" ]; then
log_warn "未配置告警Webhook URL,跳过通知"
return 1
fi
# 构建告警消息
local payload=$(cat </dev/null || true
log_info "告警通知已发送"
}
# 使用示例:在错误处理中调用
if ! critical_step; then
log_error "关键步骤执行失败"
send_alert "通知Hook" "关键步骤执行失败,请检查日志"
exit 1
fi
七、进一步思考
1. 如何实现Hook的自动化回归测试?
- 将测试用例脚本化,每次修改Hook后自动运行全部测试
- 在CI/CD流水线中加入Hook测试步骤
- 使用覆盖率工具衡量Hook脚本的测试覆盖度
2. 如何对远程服务器上的Hook进行调试?
- 使用SSH远程连接到服务器查看日志
- 通过tail -f实时监控Hook执行
- 配置远程日志聚合系统(如ELK)集中管理日志
3. 如何处理Hook执行中的并发和竞态条件?
- 使用文件锁(flock)防止多个Hook实例同时执行
- 设计幂等的Hook操作,避免重复执行产生副作用
- 使用临时文件加PID命名区分并发实例
- 考虑使用数据库或Redis作为共享状态存储
4. 如何优化Hook脚本的性能?
- 避免在Hook中执行耗时操作,使用后台任务或消息队列
- 缓存重复计算的结果,减少重复IO操作
- 使用更高效的工具(如jq代替grep+awk处理JSON)
- 设置合理的超时值,避免Hook长时间挂起
5. Hook调试的未来趋势?
- 可视化Hook执行流程图和调用链
- IDE插件支持Hook断点调试
- AI辅助的异常检测和根因分析
- 分布式Hook追踪和全景监控