Hook脚本开发基础(Shell/Python)

掌握Hook脚本开发基础

一、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 无法解析 importos.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脚本的跨平台优势

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

调试方法论:
  1. 分离测试: 将Hook逻辑拆分为独立函数,分别测试每个函数
  2. 输入验证: 确认事件数据JSON结构符合预期,使用 jq . 格式化查看
  3. 最小化复现: 创建最小的事件数据文件,反复触发Hook测试
  4. 退出码追踪: Hook执行完后检查退出码:echo $?
  5. 模拟演练: 使用 bash -x hook.shpython -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检查退出码: $?"
注意事项:

核心要点: 调试输出通过stderr分离(避免干扰stdout),使用日志文件记录详细执行过程,临时文件保存中间状态便于分析。复杂Hook应遵循"分步测试-验证输入-追踪退出码"的调试流程。善用 bash -xHOOK_DEBUG 环境变量控制调试粒度。