用于构建Claude Code hooks的Python工具包

Hacker News Top 工具

摘要

一个减少构建Claude Code hooks样板代码的Python工具包,提供类型安全的事件处理器,用于工具使用前/后、提示提交和会话开始钩子。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/05/29 07:16

RasmusGodske/claude-hook-utils 来源:https://github.com/RasmusGodske/claude-hook-utils

claude-hook-utils

一个用于构建 Claude Code 钩子(https://docs.anthropic.com/en/docs/claude-code/hooks)的 Python 工具包,只需极少的样板代码。

什么是 Claude Code 钩子?

Claude Code 钩子是在 Claude Code 执行过程中的特定时机运行的自定义脚本。它们允许你:

  • 验证工具调用在执行之前(PreToolUse)
  • 响应工具调用执行后的结果(PostToolUse)
  • 拦截用户提示在 Claude 看到之前(UserPromptSubmit)
  • 初始化会话启动时的状态(SessionStart)

为什么需要这个包?

构建 Claude Code 钩子涉及重复的样板代码:

  • 从 stdin 解析 JSON
  • 验证输入结构
  • 将响应格式化为正确的模式
  • 优雅地处理错误

claude-hook-utils 处理了所有这些,让你专注于验证逻辑。

设计理念

  1. 单一模式 - 扩展 HookHandler,覆盖你需要的钩子
  2. 类型安全 - 输入使用带类型的 dataclass,响应使用构建器模式
  3. 显式控制 - 输入上的辅助方法,但由你决定何时跳过/允许/拒绝
  4. 多钩子支持 - 一个 Python 程序可以处理多种钩子类型
  5. 无繁重依赖 - 核心包依赖极少;可按需引入自己的 AI SDK

安装

pip install claude-hook-utils

快速开始

#!/usr/bin/env python3
"""验证 Data 类是否具有 TypeScript 注解。"""

from claude_hook_utils import HookHandler, PreToolUseInput, PreToolUseResponse

class DataClassValidator(HookHandler):
    def pre_tool_use(self, input: PreToolUseInput) -> PreToolUseResponse | None:
        # 如果不是 Data 类文件则跳过
        if not input.file_path_matches('**/app/Data/**/*.php'):
            return None
        # 检查必需注解
        if input.content and '#[TypeScript()]' not in input.content:
            return PreToolUseResponse.deny(
                "Data 类必须包含 #[TypeScript()] 注解以便类型生成"
            )
        return PreToolUseResponse.allow()

if __name__ == "__main__":
    DataClassValidator().run()

.claude/settings.json 中配置:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "python3 /path/to/data_class_validator.py"
          }
        ]
      }
    ]
  }
}

支持的钩子类型

钩子类型运行时机使用场景
PreToolUse工具执行前验证文件路径、检查内容、阻止危险操作
PostToolUse工具执行后记录结果、触发后续操作、收集指标
UserPromptSubmit用户提交提示时验证提示、添加上下文、执行策略
SessionStartClaude Code 会话开始时初始化状态、设置环境变量

API 参考

HookHandler 基类

扩展此类并覆盖你需要的钩子:

from claude_hook_utils import HookHandler

class MyHandler(HookHandler):
    def __init__(self):
        super().__init__()
        # 在此添加共享状态
        self._cache: dict = {}

    def pre_tool_use(self, input: PreToolUseInput) -> PreToolUseResponse | None:
        """在工具执行前调用。返回 None 以跳过。"""
        return None

    def post_tool_use(self, input: PostToolUseInput) -> PostToolUseResponse | None:
        """在工具执行后调用。返回 None 以跳过。"""
        return None

    def user_prompt_submit(self, input: UserPromptSubmitInput) -> UserPromptSubmitResponse | None:
        """当用户提交提示时调用。返回 None 以跳过。"""
        return None

    def session_start(self, input: SessionStartInput) -> SessionStartResponse | None:
        """当会话启动时调用。返回 None 以跳过。"""
        return None

if __name__ == "__main__":
    MyHandler().run()

PreToolUseInput

PreToolUse 钩子的输入:

@dataclass
class PreToolUseInput:
    # 通用字段
    session_id: str
    cwd: str
    hook_event_name: str  # 始终为 "PreToolUse"
    # PreToolUse 特有的
    tool_name: str  # "Write", "Edit", "Bash" 等
    tool_input: dict  # 工具特定的参数
    tool_use_id: str

    # 辅助方法
    def file_path_matches(self, *globs: str) -> bool:
        """检查 tool_input.file_path 是否匹配任何 glob 模式。"""
    def file_path_excludes(self, *globs: str) -> bool:
        """检查 tool_input.file_path 是否**不**匹配任何 glob 模式。"""

    # 便捷属性
    @property
    def file_path(self) -> str | None:
        """从 tool_input 获取 file_path(适用于 Write/Edit/Read 工具)。"""
    @property
    def content(self) -> str | None:
        """从 tool_input 获取 content(适用于 Write 工具)。"""
    @property
    def command(self) -> str | None:
        """从 tool_input 获取 command(适用于 Bash 工具)。"""

PreToolUseResponse

PreToolUse 钩子的响应构建器:

class PreToolUseResponse:
    @staticmethod
    def allow(reason: str | None = None) -> PreToolUseResponse:
        """允许工具执行。"""
    @staticmethod
    def deny(reason: str) -> PreToolUseResponse:
        """阻止工具。原因会作为反馈显示给 Claude。"""
    @staticmethod
    def ask(reason: str) -> PreToolUseResponse:
        """请求用户确认后再继续。"""
    def with_updated_input(self, **updates) -> PreToolUseResponse:
        """在工具执行前修改 tool_input(仅在与 allow 一起使用时有效)。"""

HookLogger

基于 JSONL 的日志记录,便于调试。日志按命名空间(插件名称)组织。

from claude_hook_utils import HookHandler, HookLogger

class MyHandler(HookHandler):
    def __init__(self):
        # 日志写入 .claude/logs/my-plugin/hooks.jsonl
        super().__init__(
            logger=HookLogger.create_default("MyHandler", namespace="my-plugin")
        )

    def pre_tool_use(self, input: PreToolUseInput) -> PreToolUseResponse | None:
        # 会话 ID 从输入中自动添加
        self.logger.info("检查文件", file_path=input.file_path)
        # ... 验证逻辑 ...
        self.logger.decision("允许", reason="验证通过")
        return PreToolUseResponse.allow()

日志格式(JSONL - 每行一个 JSON 对象):

{"ts": "2025-01-04T10:15:23.456+00:00", "level": "INFO", "hook": "MyHandler", "namespace": "my-plugin", "session": "abc123", "msg": "检查文件", "file_path": "/path/to/file.php"}
{"ts": "2025-01-04T10:15:23.458+00:00", "level": "DECISION", "hook": "MyHandler", "namespace": "my-plugin", "session": "abc123", "msg": "decision=allow", "decision": "allow", "reason": "验证通过"}

配置:

  • 默认位置:{cwd}/.claude/logs/{namespace}/hooks.jsonl
  • 无命名空间:{cwd}/.claude/logs/hooks.jsonl
  • 通过环境变量 CLAUDE_HOOK_LOG_DIR 覆盖日志目录
  • 通过环境变量 CLAUDE_HOOK_LOG_NAMESPACE 覆盖日志命名空间/子目录
  • 会话 ID 自动从钩子输入中提取

示例

验证 Vue 组件结构

class VueValidator(HookHandler):
    def pre_tool_use(self, input: PreToolUseInput) -> PreToolUseResponse | None:
        if not input.file_path_matches('**/*.vue'):
            return None
        content = input.content or ''
        # 检查标签顺序:<template> 在 <style> 之前
        script_pos = content.find('<script')
        template_pos = content.find('<template')
        style_pos = content.find('<style')
        if script_pos > template_pos or template_pos > style_pos:
            return PreToolUseResponse.deny(
                "Vue 组件必须按顺序包含标签:<template>, <script>, <style>"
            )
        # 检查是否使用了 setup lang="ts"
        if '<script setup lang="ts">' not in content:
            return PreToolUseResponse.deny(
                'Vue 组件必须使用 <script setup lang="ts">'
            )
        return PreToolUseResponse.allow()

验证控制器位置

class ControllerValidator(HookHandler):
    def pre_tool_use(self, input: PreToolUseInput) -> PreToolUseResponse | None:
        if not input.file_path_matches('**/*Controller.php'):
            return None
        # 控制器必须位于 app/Http/Controllers/ 下
        if not input.file_path_matches('**/app/Http/Controllers/**/*.php'):
            return PreToolUseResponse.deny(
                f"控制器必须位于 app/Http/Controllers/ 中。"
                f"实际路径:{input.file_path}"
            )
        return PreToolUseResponse.allow()

禁止使用 FormRequest(推荐 Data 类)

class NoFormRequestValidator(HookHandler):
    def pre_tool_use(self, input: PreToolUseInput) -> PreToolUseResponse | None:
        if not input.file_path_matches('**/*Controller.php'):
            return None
        content = input.content or ''
        if 'FormRequest' in content:
            return PreToolUseResponse.deny(
                "请勿使用 FormRequest 类。请改用 Data 类。"
                "示例参见:app/Data/"
            )
        return PreToolUseResponse.allow()

多钩子处理器(Pre + Post)

class FileTracker(HookHandler):
    def __init__(self):
        super().__init__()
        self._pending_writes: set[str] = set()

    def pre_tool_use(self, input: PreToolUseInput) -> PreToolUseResponse | None:
        if input.tool_name == 'Write' and input.file_path:
            self._pending_writes.add(input.file_path)
            self.logger.info(f"跟踪写入:{input.file_path}")
        return PreToolUseResponse.allow()

    def post_tool_use(self, input: PostToolUseInput) -> PostToolUseResponse | None:
        if input.tool_name == 'Write' and input.file_path:
            self._pending_writes.discard(input.file_path)
            self.logger.info(f"写入完成:{input.file_path}")
        return None

Claude Code 钩子响应格式

本包生成符合官方 hookSpecificOutput 格式的响应:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "你的原因在此"
  }
}

决策选项

决策效果
allow工具立即执行,原因显示给用户
deny工具被阻止,原因显示给 Claude(以便其调整)
ask显示用户确认对话框

修改工具输入

使用 with_updated_input() 在工具执行前修改参数:

def pre_tool_use(self, input: PreToolUseInput) -> PreToolUseResponse | None:
    # 自动修正一个常见错误
    if input.file_path and '/data/' in input.file_path:
        corrected = input.file_path.replace('/data/', '/Data/')
        return PreToolUseResponse.allow("已自动修正路径").with_updated_input(
            file_path=corrected
        )
    return PreToolUseResponse.allow()

错误处理

本包优雅地处理错误:

  • 无效的 JSON 输入:返回退出码 0(无输出 = 允许)
  • 未知的钩子类型:返回 None(跳过)
  • 处理器中的异常:记录到 stderr,返回退出码 0(故障时开放)

这种“故障开放“方式确保即使出现错误,你的钩子也不会阻塞 Claude Code。

环境变量

Claude Code 向钩子提供以下环境变量:

变量描述
CLAUDE_PROJECT_DIR项目根目录的绝对路径
CLAUDE_CODE_REMOTE如果在 Web 环境中运行则为 "true"

本包使用:

变量描述
CLAUDE_HOOK_LOG_DIR覆盖默认日志目录(默认:.claude/logs/{namespace}/
CLAUDE_HOOK_LOG_NAMESPACE覆盖日志命名空间/子目录

通过 input.cwdos.environ 访问。

为新的钩子类型扩展支持

要添加对新钩子类型的支持:

  1. inputs/ 中创建输入数据类
  2. responses/ 中创建响应类
  3. HookHandler 中添加处理器方法
  4. HookHandler._dispatch() 中添加分发分支

参见现有实现以了解模式。

许可证

MIT

相似文章

@vincemask: 很多人不知道什么时候该用 Hooks。 我的判断很简单:凡是你需要反复提醒 Claude 的事,都应该考虑从 prompt 里拿出来,交给 Hook。 比如: 1、每次改完代码都要格式化 2、每次提交前都要跑 lint / test 3、…

X AI KOLs Timeline

文章介绍了在Claude AI编程辅助中,何时应该使用Hooks来固化重复性规则(如自动格式化、提交前检查等),建议将稳定、重复、易忘的流程从Prompt中剥离交给环境默认执行。

anthropics/claude-code

GitHub Trending (daily)

Claude Code 是 Anthropic 推出的一款代理式编码工具,它运行在终端中,能够理解代码库,并通过自然语言命令帮助完成诸如执行常规编码任务、解释代码和处理 git 工作流等任务。