mcpField
H
hooks-create

hooks-create

github.com/coleam00Agent 工作流722243 次安装3 周前更新
代码仓库

关于 hooks-create

hooks-create 是一项 Claude Code 技能,可将需求转化为生命周期钩子、脚本和设置配置。它指导选择事件、匹配范围、实现与验证,并明确钩子是否应阻止操作。

主要功能

  1. 01将需求映射到生命周期事件
  2. 02选择事件匹配器和阻止行为
  3. 03编写并配置钩子脚本
  4. 04合并钩子设置而不覆盖现有项
  5. 05测试允许与阻止行为

适用场景

  1. 01阻止访问指定文件
  2. 02在 Agent 完成前运行检查
  3. 03在会话开始时注入上下文
  4. 04对工具活动执行格式化或记录

MCP 相关

该技能将钩子安全注意事项与 MCP 服务的安全提醒相提并论,但未说明 MCP 连接或服务管理功能。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/coleam00/skills --skill hooks-create

安装后在 Claude Code 中输入/hooks-create确认已加载。

详细介绍

创建 Hook:将想法变成可用的 Claude Code Hook

用户希望 Hook 执行的操作:$ARGUMENTS

如果已填写,请将其视为行为规范,并以此为起点。不要再次询问用户已经说明的内容;只需确认尚不明确的部分(确切路径 / 命令 / 模式,以及是否必须阻止)。如果为空,请先询问 Hook 应保证什么或执行什么操作(工作流第 1 步)。

什么是 Hook(30 秒简介)

Hook 是一种确定性代码,会在 Claude Code 生命周期事件发生时自动触发——例如工具运行前、工具运行后、Agent 尝试结束时、会话开始时等。与技能或子 Agent 不同,Agent 无法自行选择是否调用 Hook——无论模型是否“记得”,Hook 都会触发。这正是它的意义:规则只是要求 Agent 遵循行为;而 Hook 会在模型无法绕过的工具层面保证行为得到执行。

用户提出想法(“Agent 绝不能读取我的 .env”);此技能负责编写代码并完成接入。用户不需要懂 Python。

一切都是组合——Hook 只是一个小脚本,由运行框架在指定时机执行,并在 .claude/settings.json 中配置。你正在 AI 层添加另一项确定性保障。

最重要的一点:选对事件,并确认它能否阻止操作

用户想要的行为对应一个生命周期事件。根据触发时机和是否必须停止某项操作来选择:

用户想要……事件能否阻止?
阻止 Agent 执行某项操作(读取机密、编辑受保护路径、运行破坏性命令)PreToolUse ⭐可以——在工具运行前阻止它
在操作后做出响应(自动格式化已编辑的文件、记录命令、注入上下文)PostToolUse不可以——工具已经运行;只能观察 / 格式化 / 注入
保证检查通过后才能“完成”工作(测试 / lint / 类型检查通过后才能停止)Stop(或 SubagentStop)可以——阻止停止,并让 Agent 返回工作状态
在模型看到用户提示词之前对其进行检查或门控UserPromptSubmit可以——阻止提示词;也可以注入上下文
每次会话开始时加载上下文SessionStart不可以——只能注入上下文
在 Agent 需要你协助 / 完成时收到通知Notification(或 Stop)不可以——只能执行副作用(桌面通知 / Slack / 声音)
在上下文压缩前快照保存状态PreCompact不可以

Pre = 保证 / 门控。Post = 响应 / 记录。 如果用户的目标是“确保 X 绝不会发生”或“直到 Y 才能完成”,这就是一个阻止型 Hook(PreToolUse / Stop / UserPromptSubmit)。如果目标是“在 W 发生时执行 Z”,这就是一个观察 / 响应型 Hook(PostToolUse / SessionStart / Notification)。

必读内容(先执行此步骤)

Hook 事件列表和精确的 stdin/stdout 约定会不断变化——不要依赖旧版本快照。编写之前,获取当前文档,确认事件名称、输入字段和控制协议:

对以上页面使用 WebFetch,并对照即将编写的内容进行核实。如果获取失败,请基于上表中的规范事件继续,并在报告中说明这一点,以便用户再次确认。

执行协议(钩子如何与 Claude Code 通信)

  • 输入: Claude Code 会通过 stdin 传入一个 JSON 对象,其中始终包含 session_id、cwd、 hook_event_name;还会包含与事件相关的字段,例如 tool_name + tool_input(工具事件)、prompt (UserPromptSubmit)、source(SessionStart)。
  • 输出 / 控制:
    • exit 0 - 允许 / 成功。对于 UserPromptSubmit 和 SessionStart,打印到 stdout 的任何内容都会 注入 Agent 的上下文。
    • exit 2 - 阻止。 操作会被拦截,打印到 stderr 的内容会作为原因反馈给 Agent,使其调整行为。 (只有支持阻止的事件才会遵循此设置 - 请参阅表格。)
    • 任何其他退出码 - 非阻止性错误;会显示给用户,但执行会继续。
    • 高级选项(可选): 不使用退出码,而是在 stdout 上打印一个 JSON 对象 - 例如 {"decision":"block", "reason":"…"}(Stop),或 {"hookSpecificOutput":{"hookEventName":"PreToolUse", "permissionDecision":"deny","permissionDecisionReason":"…"}}。除非用户需要修改输入/输出,或通过 additionalContext 注入上下文,否则优先使用简单的退出码形式。请对照获取的文档确认字段名称。

工作流

1. 理解需求(从 $ARGUMENTS 开始;仅在信息缺失时询问)

先从用户在 $ARGUMENTS 中已有的描述开始(用户可能并不熟悉技术)。用通俗语言明确两点,只询问缺失的信息:

  • 要执行或阻止什么,以及何时执行(操作前/后、结束时、会话开始时)?
  • 匹配要多精确?(“任何 .env 文件”、“migrations/ 文件夹”、“rm -rf”、“我的测试命令以非零状态退出”。)获取具体的文件路径 / 命令 / 模式 - 保证效果取决于匹配范围,因此不要猜测。如果需求含糊,提出一个具体解释并请用户确认。
2. 阅读文档

获取钩子参考文档 / 指南(见上文),并确认目标事件的名称、stdin 字段和控制协议。

3. 选择事件(和匹配器)

使用表格选择一个事件。选择能严格限定范围的匹配器 - 对于工具事件,使用工具名称(例如 "Bash"、"Edit|Write"、 "mcp__.*");空值 / "*" 表示每次出现都匹配。如果目标明确,不要对所有情况都触发。

4. 编写钩子脚本
  • 默认使用 uv 单文件 Python 脚本,路径为 .claude/hooks/<event_snake_case>.py - 使用 uv run --script shebang 和内联 # /// script 元数据,这样无需安装,也不需要单独的项目 venv。只有用户提出要求时才使用其他语言。
  • 从 stdin 读取 JSON,执行检查,并:
    • 要阻止:向 stderr 打印清楚的原因,并执行 sys.exit(2);
    • 要允许:执行 sys.exit(0)(对于会注入上下文的事件,可选择向 stdout 打印上下文)。
  • 失败时放行。 将主体代码包在错误处理逻辑中,确保任何意外错误都会以 0 退出(损坏的钩子绝不能让用户的 会话无法继续)。唯一有意使用的非零退出是主动阻止时的 exit 2。
  • 保持精简、易读 - 用户之后可能需要调整匹配的路径 / 命令。
  • 如果该事件已有钩子,扩展而不是覆盖(添加你的检查,并保留原有逻辑)。
  • 仅适用于 Stop / SubagentStop: 首先检查 stop_hook_active;如果为 true,则执行 sys.exit(0)。没有 此保护时,钩子会阻止停止,Agent 继续工作并再次尝试停止,又会再次被阻止 - 从而陷入循环。
⚠️ 从钩子中运行项目命令(编写钩子前请阅读)

钩子本身在**uv run 启动的隔离临时环境中**运行。它使用的解释器不是项目的解释器,也没有项目依赖。因此:

  • 绝不要用 sys.executable 或 ["python", "-m", "pytest", ...] 这样的裸列表重建命令。这样运行的是钩子用的临时 Python,它没有 pytest、没有项目包,什么都没有。命令每次都会因 No module named … 而失败——因此,“测试通过前不要结束”的钩子,无论测试通过还是失败都会拦截,并给出荒谬的原因。看起来好像能用,其实不能。
  • 务必在项目目录中,将用户的命令原样作为 shell 字符串运行——移除 uv 的临时 venv,并将项目自己的 venv 放在最前面。这两个步骤缺一不可。仅设置 shell=True 不够:uv run 会把它的临时解释器放在 PATH 最前面,并设置 VIRTUAL_ENV。但移除 uv 的 venv 并不会激活项目的 venv——钩子不是用户的 shell,因此 .venv 一开始就不在 PATH 中,python 会退回到机器上的全局解释器。全局解释器的包集不同(而且往往有问题),所以即使测试套件完全通过,钩子也会以状态码 2 退出,并归咎于无关模块。请原样复制此辅助函数:
    TEST_COMMAND = "python -m pytest -q"   # 用户输入的原始命令;用户只需编辑这一行
    
    def _project_env(project_root: Path) -> dict:
        """移除 uv 的临时 venv,并将项目自己的 venv 放在最前面的 os.environ。"""
        env = os.environ.copy()
        ephemeral = env.pop("VIRTUAL_ENV", None)
        parts = env.get("PATH", "").split(os.pathsep)
        if ephemeral:                                    # 1. 移除 uv 的临时 venv
            drop = {os.path.join(ephemeral, "Scripts"), os.path.join(ephemeral, "bin")}
            parts = [p for p in parts if p not in drop]
        for candidate in (".venv", "venv"):              # 2. 将项目自己的 venv 放在最前面
            for bindir in ("Scripts", "bin"):
                venv_bin = project_root / candidate / bindir
                if venv_bin.is_dir():
                    env["VIRTUAL_ENV"] = str(project_root / candidate)
                    parts.insert(0, str(venv_bin))
                    env["PATH"] = os.pathsep.join(parts)
                    return env
        env["PATH"] = os.pathsep.join(parts)
        return env
    
    root = Path(hook_input["cwd"])          # Claude Code 传入的项目根目录
    result = subprocess.run(
        TEST_COMMAND, shell=True, capture_output=True, text=True,
        cwd=str(root), env=_project_env(root),
    )
    
    将命令放在文件顶部一个单独命名的常量中,让用户能轻松找到并编辑这一行。
  • 如果命令必须从子目录运行(例如,monorepo,或测试配置位于更深层目录的项目,比如 app/backend/),请询问用户并传入该目录:cwd=Path(hook_input["cwd"]) / "app/backend"。目录弄错会导致钩子始终拦截,用户会以为“钩子坏了”。
5. 将其接入 settings.json

编辑 .claude/settings.json(如果文件不存在,则创建)。将内容合并到任何现有的 hooks 块中——绝不要覆盖其他事件或同一事件上的其他钩子。结构如下:

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Edit|Write|Read|Bash",
        "hooks": [ { "type": "command", "command": "uv run .claude/hooks/pre_tool_use.py" } ] }
    ]
  }
}
6. 亲自验证,然后解释并提醒

交付前先运行钩子。 通过 stdin 向钩子输入一个示例事件,并检查退出代码——不要交付一个你只读过、却从未运行过的钩子。一个总是阻止操作或从不阻止操作的钩子,在错误时机触发之前,看起来和正常工作的钩子没有区别。

# should ALLOW (exit 0)
echo '{"session_id":"t","cwd":"<project-root>","hook_event_name":"Stop","stop_hook_active":false}' | uv run .claude/hooks/stop.py; echo "exit=$?"
  • 对于运行命令的钩子(测试/lint/类型检查),这是强制要求,而且必须检查两个方向:命令通过时退出代码为 0,命令确实失败时退出代码为 2。如果两种情况下都退出代码为 2,说明命令未能解析——重新阅读第 4 步中关于 sys.executable 的警告。
  • 对于阻止操作的保护钩子,输入一个应该被阻止的载荷,再输入一个应该通过的载荷。
  • 如果检查结果不正确,先修复脚本并重新运行,再报告成功。

然后:

  • 用通俗的话告诉用户你构建了什么:钩子对应哪个事件、它能保证什么,以及他们可以改哪一行来调整它。
  • 告诉他们如何在 Agent 中验证:对于阻止操作的钩子,说明一个应该被阻止的操作(“让我读取 env 文件——观察它拒绝操作”);对于观察类钩子,说明输出会出现在哪里(日志或通知)。
  • 报告你验证了什么,并明确说明无法验证的部分。
  • 安全说明(务必说明): 钩子会在每次匹配事件发生时自动运行任意代码,并使用你的凭据。像审查 CI 配置一样审查钩子;只运行你信任的钩子。(与 MCP 服务的注意事项相同。)

质量检查

  • ✅ 行为对应正确的事件,阻止操作的目标使用支持阻止操作的事件(PreToolUse/Stop/UserPromptSubmit),而不是 PostToolUse。
  • ✅ 匹配器的范围符合用户的实际意图(不会意外地对所有内容触发)。
  • ✅ 脚本采用失败时放行策略——任何错误都以 0 退出;只有在确实要阻止操作时才以 exit 2 退出,并通过 stderr 给出清晰的原因。
  • ✅ 任何项目命令都通过 shell=True 在项目的 cwd 中原样运行,从 PATH 中移除 uv 的临时 venv,并将项目自己的 venv 放在 PATH 首位——绝不能用 sys.executable 重新构造命令(钩子自己的解释器没有项目依赖,而全局解释器中的依赖又不正确)。
  • ✅ Stop/SubagentStop 钩子会遵守 stop_hook_active,避免陷入循环。
  • ✅ settings.json 已与现有内容合并,而非覆盖;现有钩子仍然保留。
  • ✅ 你已运行钩子,并确认它在应该放行时以 0 退出、在应该阻止时以 2 退出——而不只是读过它。
  • ✅ 用户收到了通俗易懂的说明、测试方法和安全说明。

备注

  • 钩子是 AI Layer 中确定性的基础层——将它用于不可妥协的要求(机密信息、受保护路径、“全部通过后才能结束”),而不是用于规则或技能足以处理的事项。
  • 阻止操作的钩子能覆盖多大范围,取决于它的匹配器——它能保证钩子运行,但由你决定它会拦截什么。应如实告知用户其覆盖范围的边界(例如,匹配 .env 无法捕获经过 base64 编码的读取操作)。
  • 钩子要保持快速——每次匹配事件发生时都会运行。耗时较长的工作应交给异步钩子或技能。

评论

支持 Markdown