Claude Code Hooks 怎么用,关键不在于记住所有事件,而在于待自动化的动作是否有明确的触发时机和唯一结果。项目已有的配置语法检查适合接入 Hook:文件改完后运行同一条命令,有效配置返回成功,无效配置返回确定错误,不需要模型选择标准。

项目规则说明配置文件位置和固定命令,Prompt 描述本次变化,Skill 处理现场判断。Hook 只在匹配事件发生时调用项目命令;Claude Code 与 Codex 各自维护事件配置,共用同一校验逻辑。

什么动作适合放进 Hook

一项动作适合放进 Hook,需要同时满足四个条件:触发时机可以由客户端事件表达;相同项目状态会得到相同结果;重复运行不会继续修改状态;失败后能够给出明确诊断。

“找到这次改动的责任模块”“判断是否需要迁移数据库”“决定哪些消费者受影响”都依赖代码和需求,不能压进固定脚本。它们应由当前任务、项目规则或 Skill 约束。Hook 可以在这些判断完成后运行确定性检查,不能代替判断本身。

以下示例假设项目已经用 npm run check:runtime-config 校验 config/runtime.json。解析器只有在文件写入后才能读取磁盘上的完整内容,所以这项检查使用 PostToolUse,不在修改前猜测模型将写出什么。

项目事实和完整流程不要写进 Hook

层在配置修改中的责任不应承担的责任
项目规则写明配置位置、项目命令和不可破坏的边界保存某次需求或复制 Hook 脚本
当前 Prompt描述本次字段变化、现状、范围和目标结果重复长期命令与通用流程
Skill读取现场,定位配置消费者并处理需要判断的分支保证每次写入后都记得执行机械动作
Hook在匹配事件上调用确定性动作并返回运行结果决定业务规则、判断整个任务是否完成
权限与 Sandbox限制工具、路径、网络和命令可以做什么判断软件结果是否正确
CI 与服务端授权在客户端之外决定是否允许合并、部署和业务操作为本地编辑提供即时上下文

项目规则保留 npm run check:runtime-config 这条长期事实,Prompt 写本次字段变化,Skill 识别消费者并处理分支。Hook 负责在写入后自动执行命令;只把命令写进 Skill,未加载该 Skill 时仍会漏跑。

假设当前 Prompt 要把端口从 3000 改为 3100。Skill 先读取配置消费者,确认端口仍由 config/runtime.json 提供,再让文件工具完成修改。PostToolUse 随后调用适配脚本,项目命令读取写入后的完整文件:整数端口通过,字符串 "3100" 返回失败,错误信息交回客户端供 Agent 修正。是否应该改端口、服务能否访问,仍要由任务上下文和运行结果判断;Hook 只负责这一次配置校验。

校验命令仍放在项目工具链中

校验离开任何 Coding Agent 也要能独立运行。这组样例需要 Git、Node.js、npm 和 Bash;Windows 项目可以把适配层改为 PowerShell,但 npm 校验命令保持不变。项目脚本在校验失败时按普通命令行约定返回非零,不处理任何客户端事件。

六个文件都放在现有项目中,各自只有一项责任:

.
├── config/runtime.json
├── package.json
├── scripts/check-runtime-config.mjs
├── scripts/agent-hooks/check-runtime-config.sh
├── .claude/settings.json
└── .codex/hooks.json

runtime.json 保存配置,Node.js 脚本定义校验规则,Shell 脚本只检查 Git 状态,并把失败转换为客户端需要的退出码;两个设置文件只描述各自的事件。这里不需要另建一套配置仓库。

已有 package.json 时,只把 check:runtime-config 合并进现有 scripts:

{
  "private": true,
  "scripts": {
    "check:runtime-config": "node scripts/check-runtime-config.mjs"
  }
}

scripts/check-runtime-config.mjs 解析项目配置,并检查这个示例真正依赖的两个字段:

#!/usr/bin/env node

import { readFileSync } from "node:fs";

try {
  const config = JSON.parse(
    readFileSync(new URL("../config/runtime.json", import.meta.url), "utf8"),
  );

  if (!["development", "production"].includes(config.mode)) {
    throw new Error("mode must be development or production");
  }
  if (!Number.isInteger(config.port) || config.port < 1 || config.port > 65535) {
    throw new Error("port must be an integer between 1 and 65535");
  }
} catch (error) {
  console.error(`config/runtime.json: ${error.message}`);
  process.exit(1);
}

config/runtime.json 先使用一份可以解析的内容:

{
  "mode": "development",
  "port": 3000
}

先在项目根目录直接运行一次:

npm run check:runtime-config

有效文件应返回 0;把端口改为字符串或加入多余逗号后,应返回非零并指出 config/runtime.json。前者验证字段规则,后者验证 JSON 语法。如果直接命令无法稳定区分成功和失败,接入 Hook 只会把含糊结果自动重复,应该先修项目命令。

测试、CI 和开发者手工检查都调用这条 npm 命令。Hook 需要的“目标文件未变就立即返回”和“把校验失败转换为客户端能识别的反馈”留在客户端适配脚本,不改变项目命令的通用退出约定。

项目命令必须覆盖真正影响运行的约束。若这里只调用 JSON.parse,{"mode":"unknown","port":"3100"} 仍会通过语法检查,却可能在服务启动后失败。这个示例检查运行模式和端口;当前项目应复用配置模块已有的 schema 或验证器,不要在 Hook 中再复制一套枚举、范围和跨字段规则。依赖网络、凭据或长时间运行的验证留给集成测试或 CI,不要塞进每次写入都会触发的快速检查。

Node.js 脚本按命令行惯例用 1 表示校验失败,CI 可以直接识别;适配脚本再把同一失败转换成两个客户端在 PostToolUse 中使用的退出码 2。两层只要求不同的退出码,校验规则和错误内容仍来自同一条项目命令。

Hook 的作用域决定谁会执行

团队共同依赖的配置检查应随项目维护。Claude Code 的项目级位置是 .claude/settings.json;本机设置可以放进 ~/.claude/settings.json 或 .claude/settings.local.json,各位置的范围见 Claude Code Hooks 参考(在新标签页打开)。

Codex 从用户层的 ~/.codex/hooks.json、~/.codex/config.toml,以及项目层的 .codex/hooks.json、.codex/config.toml 读取 Hook。多个来源中匹配的 Hook 都会运行,上层不会覆盖下层;同一层同时使用 JSON 与内联 TOML 会产生合并警告,因此每层只保留一种表达方式(Codex Hooks 文档(在新标签页打开))。

客户端与位置实际范围适合放什么
Claude Code ~/.claude/settings.json当前用户的所有项目个人通知、本机命令与个人偏好
Claude Code .claude/settings.json信任该项目的团队成员随项目共享的格式、测试与配置检查
Claude Code .claude/settings.local.json当前机器上的这个项目不应提交的本机路径或临时个人设置
Codex ~/.codex/hooks.json 或 config.toml当前用户的 Codex 会话个人通知、本机命令与个人偏好
Codex .codex/hooks.json 或 config.toml已信任项目层和当前 Hook 定义的成员随项目共享的确定性检查

这项配置语法检查对所有成员都应生效,因此两端入口都放在项目目录。个人通知、私人日志和本机路径留在用户层;接入这项检查不需要先建设独立配置仓库。

项目 Hook 会执行仓库内命令。Codex 只在项目 .codex/ 层受信任后加载项目 Hook,并按当前定义的哈希记录信任;命令变化后需要重新审查(Codex Hooks 文档(在新标签页打开))。Claude Code 的交互会话受工作区信任影响,但 -p 和 SDK 会话不同(Claude Code Hooks 参考(在新标签页打开))。打开陌生仓库前,先读取其中的 .claude/ 与 .codex/ 配置。

从动作发生时机选择事件

事件应由动作发生的时机决定。语法检查读取写入后的完整文件,所以使用 PostToolUse;若要在命令执行前阻止一类危险调用,才考虑 PreToolUse。两者互换会改变结果:前置事件可以阻止尚未发生的动作,后置事件只能反馈已经发生的结果。

PostToolUse 的 matcher 过滤工具名,不是文件路径。Write|Edit 能覆盖 Claude Code 的对应文件工具;Codex 也允许用这两个名称匹配 apply_patch。Bash、其他专用工具或外部进程写入不会自动命中这个 matcher(Claude Code Hooks 参考(在新标签页打开);Codex Hooks 文档(在新标签页打开))。

适配脚本仍要检查目标文件是否处于改动状态,matcher 只按工具名粗略筛选。项目允许多种写入路径时,增加对应工具事件,并让提交前检查与 CI 继续兜底。

这项 Hook 要在下一次模型调用前返回失败。耗时测试应缩小范围,或移到任务停止、提交前和 CI;异步任务无法在下一次模型调用前阻止流程。

Claude Code 怎样接入这项校验

在项目的 .claude/settings.json 中加入 PostToolUse。已有设置时,只把对应项合并进现有 hooks,不要用整段示例覆盖原文件;Codex 配置也按相同原则合并。${CLAUDE_PROJECT_DIR} 让会话从子目录启动时仍能找到项目脚本。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash \"${CLAUDE_PROJECT_DIR}/scripts/agent-hooks/check-runtime-config.sh\"",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

Claude Code 把 Hook 输入以 JSON 写入标准输入;适配脚本改用 Git 状态判断目标文件,所以不读取工具参数。如果检查必须读取本次工具参数,就按该事件的结构解析,并在字段缺失时返回错误。

运行 /hooks 可以看到来源、事件、matcher 和完整命令。配置没有出现时检查 JSON 与设置来源;已经出现但没有动作时,确认修改是否由 Write 或 Edit 完成。标准输出、错误和超时可结合 Claude Code 配置调试文档(在新标签页打开)定位。

多个匹配的 Claude Code Hook 会并行运行(Claude Code Hooks 参考(在新标签页打开)),不能让一个 Hook 依赖另一个先生成文件。适配脚本只读目标配置,并且不改变工作区。

失败时,适配脚本向标准错误写诊断并返回 2。Claude Code 会显示 PostToolUse 反馈,但不会回滚写入;返回 1 在多数事件上只是非阻断错误(Claude Code Hooks 参考(在新标签页打开))。

Codex 怎样接入同一项校验

Codex 的项目入口使用 .codex/hooks.json。这个示例在两端都使用 PostToolUse 和相近的 matcher,但配置结构仍由 Codex 定义;项目根目录通过 Git 解析,不假设会话总从仓库根启动。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "bash \"$(git rev-parse --show-toplevel)/scripts/agent-hooks/check-runtime-config.sh\"",
            "timeout": 10,
            "statusMessage": "Checking runtime configuration"
          }
        ]
      }
    ]
  }
}

Codex 的 apply_patch 当前支持 PreToolUse 与 PostToolUse,matcher 可以写 apply_patch、Edit 或 Write。事件输入中的真实 tool_name 仍可能是 apply_patch;matcher 别名不代表两个客户端拥有相同事件输入(Codex Hooks 文档(在新标签页打开))。

项目配置加入后,先打开 /hooks。项目层未受信任或 Hook 定义刚修改时,Codex 会跳过它,直到当前定义完成审查。这个状态与脚本本身是否可执行不同:脚本手工运行成功,只能证明项目命令链成立,不能证明 Codex 已加载并信任该 Hook。

Codex 会运行所有来源中匹配的 Hook,同一事件下的多个命令 Hook 并发启动(Codex Hooks 文档(在新标签页打开))。用户层已有同类检查时,项目层再加一份会执行两次;重复输出应从 /hooks 删除重复来源。

PostToolUse 返回 2 时,Codex 可以把标准错误作为反馈,但不能撤销已经完成的写入;当前 PreToolUse 对受支持调用可以阻止或改写输入(Codex Hooks 文档(在新标签页打开))。这项检查读取写入后的文件,仍使用后置事件。

同一脚本,两个客户端各自配置

两个配置都调用 scripts/agent-hooks/check-runtime-config.sh。它先找到当前 Git 项目,确认目标配置确实有改动,再运行项目自己的 npm 命令。项目命令失败时,适配层把诊断写到标准错误并返回 2,让两个客户端都能在 PostToolUse 后取得反馈。

#!/usr/bin/env bash
set -euo pipefail

target="config/runtime.json"

if ! project_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then
  printf '%s\n' "runtime-config hook must run inside the project worktree" >&2
  exit 2
fi

if ! changed="$(git -C "${project_root}" status --porcelain=v1 -- "${target}")"; then
  printf '%s\n' "runtime-config hook could not inspect ${target}" >&2
  exit 2
fi

if [[ -z "${changed}" ]]; then
  exit 0
fi

if ! output="$(npm --prefix "${project_root}" run --silent check:runtime-config 2>&1)"; then
  printf '%s\n' "${output}" >&2
  exit 2
fi
项目规则给出文件位置和校验命令,Prompt 与 Skill 决定修改内容,Claude Code 与 Codex 各自匹配 PostToolUse,再通过同一适配脚本运行校验
项目命令和失败结果可以共用;事件配置、信任和调试入口仍按客户端分别设置。

脚本不从事件输入猜测路径,也不重写 JSON。它只检查目标文件是否有改动,并把校验失败统一返回为 2;JSON 语法、文件位置和错误内容仍由项目命令决定。

只要 config/runtime.json 保持未提交改动,后续每个匹配的 Write 或 Edit 都会再次运行检查,即使那次工具调用修改了别的文件。脚本因此不依赖两端不同的文件参数。检查必须只读且足够快;若执行成本不能接受,再为两个运行时分别读取各自事件输入,并测试所依赖的字段,防止客户端升级后悄悄改变输入格式。

用受控输入验证 Hook

先验证项目命令和适配脚本,再验证客户端加载与事件反馈。“没有提示”可能来自解析器、Shell、配置来源、matcher 或信任;表中的数值只表示进程退出状态。

输入状态操作适配脚本结果客户端中应观察到的结果
目标配置未改修改无关文件返回 0,不运行项目解析器Hook 可以被 matcher 调用,但不产生配置诊断
目标配置有效且已改写入合法 JSON返回 0工具结果保留,Agent 继续处理任务
JSON 合法但字段无效把 port 写成字符串返回 2,标准错误指出端口规则Agent 收到可直接修正的字段错误;原值仍留在文件中
目标配置无效且已改加入多余逗号返回 2,标准错误含文件与解析错误Agent 收到失败反馈;错误写入仍留在工作区
同一无效状态未变化再次执行适配脚本仍返回 2,诊断一致重复执行不追加文件、不改变错误内容
从项目子目录启动修改目标配置回到 Git 根目录检查同一文件不因当前目录不同而找错脚本或配置

在一个只包含上述样例文件的临时 Git 目录中,合法改动连续两次返回 0;字符串端口连续两次返回 2,并得到同一字段错误;加入多余逗号后也连续两次返回 2,并得到同一解析错误。恢复目标文件、只改 README 后,根目录和子目录运行都返回 0。这些退出状态只覆盖适配脚本,客户端是否加载配置仍要单独观察。

客户端验证要在新的项目会话中完成。先用 /hooks 确认来源、事件、matcher、命令与信任状态,再让对应文件工具写入一个合法值,观察工具结果和后续动作;随后故意加入一个可恢复的语法错误,确认反馈出现且错误文件没有被自动还原。恢复有效内容后再运行项目命令,避免把故意构造的失败留在工作区。

无关路径也要通过客户端触发一次。目标配置干净时修改 README,适配脚本应立即返回;目标配置仍是脏状态时修改 README,脚本会再次校验目标配置。“Hook 被调用”和“项目解析器被调用”是两个状态,Git 状态筛选也不是 matcher 的文件过滤。

Hook 自己也有失败模型

Hook 配置和脚本也会失败。JSON 错误会阻止加载,命令路径错误会阻止处理器启动,多个来源会并发执行同一动作,超时则可能丢弃输出。用 /hooks、debug 日志、脚本退出状态和文件是否已经写入来定位对应层。

Hook 匹配后依次检查项目根、目标改动和项目命令,分别进入目标未改、成功、失败反馈、超时或未加载路径,PostToolUse 失败不会回滚文件
先区分未加载、未匹配、脚本未执行和项目命令失败,再修改对应的配置、适配脚本或项目命令。

超时不能当作拒绝。Claude Code 的普通命令 Hook 在 PreToolUse 超时后会继续进入权限流程;Codex 未显式设置时,多数 Hook 默认等待 600 秒(Claude Code(在新标签页打开);Codex(在新标签页打开))。示例使用 10 秒;检查经常超时时,应缩小检查范围,或改到任务停止、提交前和 CI 执行。

并发要求脚本保持幂等。两个匹配 Hook 可能同时读取同一文件,所以脚本不能自动格式化、覆盖配置或写共享临时状态。需要生成文件时,改由一个项目命令按固定顺序完成生成和校验;不能假设 Hook 数组顺序就是执行顺序。

PostToolUse 适合即时反馈,不适合回滚。无效 JSON 已经写入磁盘,后续模型可以依据错误修正,但其他进程也可能在这段时间读到它。不能容忍中间无效状态时,应让写入命令先生成临时文件、校验后原子替换,或由拥有该配置的服务提供事务接口;提高 Hook 警告强度不能提供原子性。

Hook 不能替代权限、Sandbox 和 CI

Hook 和被检查的工具都运行在同一个客户端里。配置可以被禁用,matcher 可能漏掉写入路径,命令可能超时,脚本还可能成为代码执行入口。权限与 Sandbox 限制工具、路径和网络;服务端授权决定业务操作;CI 使用同一项目命令,在独立环境中拒绝无效配置。

前置 Hook 只能阻止受支持的工具调用。攻击者或其他进程不必经过当前客户端,仓库中的恶意 Hook 还可能反过来获得执行机会。项目 Hook 必须进入代码审查,陌生项目先审查再信任;高风险操作仍要受客户端外的最小权限和服务端授权约束。

Hook 和 Skill 有什么区别? Skill 保存需要模型读取现场后执行的可复用流程;Hook 在明确事件上运行确定性动作。需要判断的步骤放进 Skill,无论模型怎样判断都必须执行的机械动作才放进 Hook。

Claude Code Hook 不触发时先检查什么? 先用 /hooks 核对配置来源、事件、matcher 和完整命令,再确认本次修改确实由 matcher 覆盖的工具完成。Write 或 Edit 不会自动覆盖 Bash 与外部进程写入。

Codex Hook 改了以后为什么不运行? 先在 /hooks 检查项目 .codex 层与修改后定义的信任状态。Codex 按当前定义的哈希记录信任,命令变化后会跳过尚未重新信任的 Hook。

Hook 能替代权限、Sandbox 和 CI 吗? 不能。Hook 在工具调用前后提供反馈或局部阻止;权限与 Sandbox 限制能力,CI 和服务端授权在客户端之外决定是否接受结果。

实际接入 Claude Code Hooks 时,先让项目命令独立运行,再选择事件并验证失败与重复路径。长期事实留在项目规则,本次变化留在任务 Prompt,需要判断的流程留在 Skill;Hook 只接管时机明确的动作。以后换用另一个客户端,只需修改它的 Hook 配置,项目命令和判断标准保持不变。