跳转到内容

04-Hook 脚本开发与调试

Hook 脚本开发与调试

学习目标

本章将掌握 Claude Code Hook 脚本的开发规范,通过 hookify 和 security-guidance 的实际代码,掌握:

  • Hook 的输入输出协议(stdin JSON 输入、stdout/stderr 输出)
  • 三种退出码的语义差异
  • Python 和 Bash Hook 脚本的实现模式
  • 调试工具链的使用

前置知识


项目实践

输入输出协议

输入:Claude Code 通过 stdin 向 Hook 脚本传递 JSON:

{
"session_id": "abc123",
"tool_name": "Edit",
"tool_input": {
"file_path": "/path/to/file.js",
"new_string": "console.log('hello')"
}
}

Stop Hook 的输入还包含 transcript_path,指向会话的 JSONL transcript 文件。

读取输入的模式

# Python
import json, sys
input_data = json.loads(sys.stdin.read())
tool_name = input_data["tool_name"]
tool_input = input_data["tool_input"]
Terminal window
# Bash
input=$(cat)
transcript_path=$(echo "$input" | jq -r '.transcript_path')

输出

输出流内容用途
stdoutJSON 响应Claude Code 解析决策
stderr用户可见的文本消息显示给用户

退出码语义

退出码含义适用事件
0允许继续(或 Hook 成功执行)所有事件
2阻止工具执行PreToolUse
1Hook 脚本错误(应避免)所有事件

关键:即使 Hook 逻辑上要阻止操作,也应以 exit 0 结束(通过 JSON 输出表达决策)。exit 2 仅用于 PreToolUse 的阻止决策。exit 1 表示 Hook 脚本自身出错。

Python Hook 模式(hookify)

#!/usr/bin/env python3
import json, sys, os
# 1. 读取输入
try:
input_data = json.loads(sys.stdin.read())
except json.JSONDecodeError:
sys.exit(0) # Fail-open
# 2. 确定事件类型
tool_name = input_data.get("tool_name", "")
if tool_name == "Bash":
event = "bash"
elif tool_name in ("Edit", "Write", "MultiEdit"):
event = "file"
else:
event = "other"
# 3. 加载规则
from core.config_loader import load_rules
rules = load_rules(event=event)
# 4. 评估规则
from core.rule_engine import RuleEngine
result = RuleEngine().evaluate_rules(rules, input_data)
# 5. 输出结果
print(json.dumps(result))
sys.exit(0) # Always exit 0

Bash Hook 模式(ralph-wiggum)

#!/bin/bash
state_file=".claude/ralph-loop.local.md"
# 检查状态文件
[ ! -f "$state_file" ] && exit 0
# 解析 frontmatter
iteration=$(grep "^iteration:" "$state_file" | awk '{print $2}')
max_iterations=$(grep "^max_iterations:" "$state_file" | awk '{print $2}')
# 检查最大迭代次数
if [ "$iteration" -ge "$max_iterations" ]; then
rm -f "$state_file"
exit 0
fi
# 检查完成承诺
# ... 解析 transcript 中的 <promise> 标签 ...
# 如果不完成 → block
new_iteration=$((iteration + 1))
sed -i '' "s/^iteration: $iteration/iteration: $new_iteration/" "$state_file"
# 输出 block JSON
jq -n \
--arg reason "$(cat "$state_file")" \
--arg msg "Ralph iteration $new_iteration" \
'{"decision": "block", "reason": $reason, "systemMessage": $msg}'
exit 0

调试工具链

plugin-dev 插件提供了 Hook 调试工具:

工具功能
validate-hook-schema.sh验证 hooks.json 的结构是否符合 schema
test-hook.sh用样本 JSON 输入测试 Hook 脚本,验证输出
hook-linter.sh检查 Hook 脚本的最佳实践(Fail-open、错误处理等)

使用示例

Terminal window
# 测试 Hook 脚本
./scripts/test-hook.sh plugins/hookify/hooks/pretooluse.py \
--input '{"tool_name": "Edit", "tool_input": {"file_path": "test.js"}}'
# 验证 Hook 注册
./scripts/validate-hook-schema.sh plugins/hookify/hooks/hooks.json

陷阱与对策

陷阱表现对策
Hook 脚本抛出未捕获异常退出码非 0,操作被阻止顶层 try/except 捕获所有异常,确保 exit 0
stdin 解析失败JSONDecodeError 导致崩溃在解析前检查 stdin 非空,捕获解析异常
Bash 中 set -e 导致意外退出任何非零退出码都会终止脚本Hook 脚本中不使用 set -e,或使用 `
正则缓存导致过期规则匹配修改规则后仍然触发旧模式重启会话或手动清理缓存目录
Stop Hook 中 transcript 文件锁定无法读取 JSONL transcript使用 grep + tail -1 读取最后一行,避免锁定

设计取舍

为什么 Hook 的输入是 JSON 而非命令行参数?

JSON 可以表达嵌套结构(如 tool_input 中的完整工具参数),命令行参数难以传递复杂数据。同时,JSON 是跨语言的标准格式,Python 和 Bash 都有成熟的解析工具。

为什么 Fail-open 是强制原则?

如果 Hook 出错就阻止操作,一个有 bug 的 Hook 会完全锁死 Claude Code。Fail-open 确保 Hook 只在其正确执行时发挥作用,出错时自动放行。

参考来源

  • Claude Code Hooks 文档
  • 源码: plugins/hookify/hooks/*.py
  • 源码: plugins/security-guidance/hooks/security_reminder_hook.py
  • 源码: plugins/ralph-wiggum/hooks/stop-hook.sh
  • 源码: plugins/plugin-dev/skills/hook-development/scripts/*.sh