Hook调试与日志:故障排查指南

Hook调试和故障排查方法

在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脚本返回非零退出码,最常见也最容易排查
超时错误
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的主要风险点:

#!/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追踪和全景监控