Hook 已经配置好了,命令 /hooks 也能看到它,但接下来就开始出怪事。您发了一个请求,可 requests.log 里没有出现新的一行。或者出现了两次。有时更糟:自动动作在每次改动之后都会触发,开始给日常工作添乱。

遇到这种情况,不必把整套配置重新搭一遍。更好的办法是把观察到的现象变成一个可核实的小任务:什么应该触发这个 hook,应该留下什么样的痕迹,以及哪种情况它必须跳过。这样 Claude Code 就能检查具体文件,给出最小改动。

什么时候 hook 应该触发

这篇文章讨论的是配置文件里的命令型 hooks。这类 hook 有三个重要部分:事件、触发条件和处理器。事件回答"什么时候看",条件缩小适用的范围,处理器执行具体动作。

比如,请求日志订阅的是 UserPromptSubmit。这个事件在会话收到请求之后、模型处理请求之前就会出现。大多数情况下这是您发出的请求,但 Claude Code 自己发起的轮次也会触发它:/loop 的新一轮、后台 subagent 的汇报。所以日志里出现新的一行,能确认的东西其实不多,只是 hook 收到了请求并记录下来,仅此而已。智能体有没有把任务做对,这一行看不出来。

UserPromptSubmit 没有 matcher。如果日志只需要部分请求,那就得靠处理器自己去挑选。工具类事件的情况不一样。比如 matcher Edit|Write 选的是工具,而文件路径要另外根据调用数据去检查。按工具名写的 matcher,不代表"只处理 src 里的文件"。

把事件和结果对上号

Stop 事件意味着 Claude 已经回答完了。这个时机很适合发通知,但不能证明代码是对的,也不能证明任务真的顺利完成。

动手改之前,先写下两个简短的场景:

  • 该触发的:发一个测试请求,然后应该在 requests.log 里看到一行新记录;
  • 不该触发的:在普通编辑器里打开文件,不向 Claude Code 发新请求,确认日志没有变化。对于记录全部请求的日志来说,第二次发出的 prompt 同样应该留下记录;它不能当作反面对照。

这些就是核实的标准。Claude Code 具体会怎么答,不用提前替它想好。

没有出现新记录

从观察到的现象和预期不一致的地方开始。对日志来说,这就是测试请求之后没有出现新的一行。接下来按顺序从配置查到动作:

  1. 在 Claude Code 的交互会话里输入 /hooks,确认需要的那条记录是不是已经加载。这个菜单由您来打开;把看到的结果转给智能体。
  2. 核对事件。请求日志应该响应的是 UserPromptSubmit,而文件改动之后的动作通常挂在工具类事件上。
  3. 找到配置的源文件。它可能是用户级、项目级或本地配置;不要只比名字,还要比完整命令。同一个处理器如果在好几个配置文件里都出现,光凭这一点还不能说明它会触发两次。
  4. 检查处理器的命令:文件是否存在,命令是否适配您的操作系统,带空格的路径有没有出问题。
  5. 检查结果落地的位置。相对路径取决于进程的工作目录,所以处理器写文件的地方可能和您去找的地方不是同一处。

这里主要靠智能体来查。把观察到的现象、/hooks 的查看结果和改动的边界一起交给它:

Hook UserPromptSubmit 在 /hooks 里可以看到,但测试请求之后
requests.log 里没有新的一行。

请找到源配置和处理器。检查事件、命令路径、
工作目录,以及日志目录有没有创建。说明原因,
给出最小的修正。其余的 hooks 和配置保持不变。
改完之后,描述该触发的场景和不该触发的对照场景。
核实改动的位置

让智能体说清楚它改的是哪个文件、具体哪几行。这样您就能看出它改的是源配置,还是一份根本不会被加载的无关副本。

比如,智能体查出来:处理器往 logs/requests.log 里写,但 logs/ 这个文件夹不存在,脚本也没有创建它。最小的修正就是在写入之前创建这个文件夹。改完之后,重点检查 logs/requests.log:一个测试请求应该只增加一条记录。这只是一种可能原因的示例,不是所有空日志的通用诊断。

如果配置文件里有这条设置,但 /hooks 里看不到,原因可能出在 JSON 语法、配置作用域上,也可能是 hooks 现在根本不会运行:项目文件夹没有被标记为受信任、开启了 disableAllHooks,或者管理员只允许运行托管策略中的 hooks。不同配置文件里的 hooks 会叠加,而不是互相替换。如果记录已经加载,但没有留下痕迹,重点就要转向命令本身、输入数据和输出路径。

根据 /hooks 菜单里的检查结果分叉的 hook 诊断路径请求之后没有新的一行/hooks 里能看到这条记录吗?核对事件、来源和命令检查配置本身的问题JSON、作用域、文件夹信任检查处理器命令、stdin、输出路径否是
/hooks 里的结果决定继续往哪个层面查

这个分叉的意思很简单:只要 Claude Code 还没加载这条记录,就没必要急着去查脚本本身的问题。一旦记录能看到,说明配置已经传到了应用这一层,这时候更该检查处理器有没有启动、输入的 JSON 长什么样,以及结果本该出现在哪里。

处理器执行出错

命令型 hook 通过标准输入拿到 JSON。里面既有事件的通用字段,也有这个事件特有的数据。对于 UserPromptSubmit,请求的文本会放在 prompt 字段里。这段文本必须当作数据来读。不能把它拼进 shell 命令再执行:测试请求里可能带引号、换行,或者任何普通文本。

让 Claude Code 用虚构的输入把处理器的执行过程重放一遍。在请求里直接说清楚要覆盖的重要情况:

在会话之外单独检查 hook 处理器,用虚构的 JSON 测试。
需要覆盖这些情况:正常的和多行的 prompt、带空格的项目路径、
缺失的日志目录,以及格式不对的输入。

不要把 prompt 的文本当作代码执行。展示启动命令、退出码、
stderr,以及测试文件的实际改动。不要改动真正的工作日志。

退出码有助于了解处理器本身的执行结果,但它的含义取决于具体事件。exit 0 说明 hook 命令没有报错就结束了。这不能证明整个任务的结果是对的。exit 2 在"事前"类事件上可以阻止动作,但工具已经执行完之后再用它,就没法撤销已经发生的动作了。所以不能把带 exit 2 的某个套路盲目搬到别的事件上用。

如果单独测试成功,但会话里 hook 还是没反应,从项目目录用命令 claude --debug-file hook-debug.log 启动 Claude Code,再发一次测试请求。打开生成的 hook-debug.log,找到对应的事件、匹配到的 hooks 和退出码。这是 Claude Code 自己的日志,和您的 requests.log 是两回事。检查完就结束这个诊断会话;日常工作不带这个参数启动 Claude Code。大多数事件的普通 stdout 会进 debug log,不一定出现在 transcript 里。具体例外取决于事件本身,所以 stdout 里随手写的诊断信息,可能会影响上下文,而不是变成一份方便查看的日志。

诊断信息和正式结果分开放

刚开始排查的时候,更稳妥的做法是把诊断标记写进单独的本地文件,查完再删掉。官方文档里没有给 hooks 提供统一的 dry-run 参数;试运行模式得作为某个具体教学处理器自己的功能来实现。

Hook 多余触发了一次

一次请求之后出现两条一样的记录,可能说明重复注册了:比如,相似但不完全一样的 hooks 分别放在了用户级和项目级配置里。如果就是同一条命令,只是被重复写进了不同的文件,那它实际执行时还是只会跑一次。plugin 或 skill 里带的副本要单独算,所以菜单里显示的来源也很重要。先把 /hooks 的结果交给智能体,让它列出所有配置文件里这个事件的记录。如果配置本身没问题,就检查处理器自己是不是在一次请求里写了两次。先别急着删掉找到的第一行。

另一个常见原因是条件写得太宽。比如,hook 本该只在 src/ 里的文件被改动之后留下诊断标记,结果却对每一次 Edit 和 Write 调用都有反应。这里需要两层筛选:

  • matcher 挑出 Edit 和 Write 这两个工具;
  • 另外单独有一个条件检查工具调用参数里的路径,把 src/ 之外的都排除掉。
PostToolUse hook 的两层筛选PostToolUse 事件工具是 Edit 还是 Write?跳过这个 hook路径在 src/ 里面吗?检查调用参数不留下标记写入标记否是否是
matcher 挑出工具,另一层检查再缩小文件范围

一个检查代替不了另一个。matcher 先筛掉不合适的工具,然后检查参数的那个条件只保留 src/ 里的路径。只有两层检查都给出"是",才会出现诊断标记。

配置片段大概是这样:

"PostToolUse": [
  {
    "matcher": "Edit|Write",
    "hooks": [
      { "type": "command", "command": "python .claude/hooks/mark-source-edit.py" }
    ]
  }
]

这只是现有 settings.json 里 hooks 对象的一个元素,不是完整的 JSON 文档。路径检查应该放在处理器里;这里的 mark-source-edit.py 只是示意,脚本本身的内容文章里没有给出。这条命令假设系统里能直接调用 python,并且是从项目根目录启动的。智能体应该核对解释器、shell 和您配置里的路径;不要把这个示例当作现成可用的配置直接照抄。

这个配置片段读起来就像一句话:

  • 文件被成功改动之后
  • 如果工具是 Edit 或 Write
  • 运行 mark-source-edit.py

这条链只展示了配置里写的内容。"留标记还是跳过文件"这个决定,是处理器检查路径之后才做出的。所以 matcher 的存在只能证明选对了工具,还不能证明 src/ 的过滤真的做对了。

动作预期留下的痕迹核实的是什么
用 Edit 改动 src/card.js新增一条诊断标记该触发的工具和该触发的路径都通过了条件
用 Write 创建 src/theme.css新增一条诊断标记另一个允许的操作也被计入
用 Edit 改动 README.md没有新标记src/ 之外的文件被排除

表里的 Edit 和 Write 指的是 Claude Code 的工具。在编辑器里手动改文件不会触发它们,所以也检验不了这样的 matcher。每次动作之前先标记一下诊断文件的末尾,之后只比较新增的记录。

在边界没核实清楚之前,让动作保持无害:只留一条触发标记,不做格式化、不做阻断,也不大范围改动文件。先确认该触发的情况恰好触发一次,不该触发的一次都不触发。

多余的触发总会带来额外的工作量。不过,一个只是写文件的本地命令,本身并不会去调用模型。prompt 或 agent 类型的 hook 不一样,它们确实会用到模型。这里不需要细算开销,只要不把这两种处理器混为一谈就行。

怎样关掉这个反应

如果 hook 现在就在添乱,找到它的源记录,只从对应事件的数组里删掉这一条就行。其余的 hooks 保持不动。这一步只是停止今后的触发:它不会撤销已经执行过的命令,不会还原已经改动的文件,也不会删掉已经写进日志的那些行。

删除之前,如果打算以后再恢复,先保存一份这条记录的副本。改完之后重新打开 /hooks,确认这条记录已经不在了。如果菜单里还显示旧配置,就结束当前会话,从同一个项目重新打开一个新会话。然后执行一次对照动作,比较改动前后的日志。关掉的这个 hook 不应该再留下新的一行。

要临时关掉所有 hooks,有一个配置 "disableAllHooks": true。但这是一个总开关,不是关掉单条记录的办法。这个配置本身遵循配置文件的优先级:项目文件里的 "disableAllHooks": false 会覆盖您用户配置里的 true。另外,由组织统一管理策略下发的 hooks,普通的用户级或项目级配置是关不掉的。

别把"关掉"和"撤销改动"混为一谈

关掉的 hook 以后不会再触发。如果处理器已经改动过文件,恢复这个文件是另外一件事,只能在看过 diff 或者已保存的版本之后单独处理。

怎样把修复交给 Claude Code

一个好的任务描述要包含观察到的现象、预期的事件、具体的文件和改动的边界。有这些就够了,智能体可以去研究配置,而不用把整套自动化重写一遍:

帮我处理这个项目里的 hook 问题。

观察到的现象:一个测试请求之后,requests.log 里出现了两行。
这个会话里没有运行 /loop,也没有后台 subagent。
预期:UserPromptSubmit 应该正好新增一行。
我会附上 /hooks 的结果。请检查所有可能的配置来源
和处理器命令。

先说明原因,以及需要改动哪些文件。然后给出
最小的修正,保留其余的 hooks,并展示 diff。
核实该触发的情况和不该触发的情况。之后再说明,
怎样只关掉这一个反应。

最后您应该得到四个清楚的答案:

  • 哪个事件触发这个 hook;
  • 哪个条件用来跳过不需要的情况;
  • 源配置和处理器分别放在哪里;
  • 怎样只关掉这一个反应,不影响其他的。

还有整条核实路径:该触发的动作留下恰好一个预期的痕迹,不该触发的不留任何痕迹,处理器的错误能在针对性的诊断里看到,关掉之后就不再出现新的痕迹。整件事都很朴实。正因为如此,这样的 hook 比那种"偶尔会做点什么"的自动化更容易维护。

核对具体行为时,请用官方的 hooks 参考文档和实践指南:界面会随着 Claude Code 一起变化。

本文行为已对照官方文档核实,核实时间 2026 年 9 月 18 日。示例展示的是核实的顺序,不是一个现成可用的处理器。命令和路径请在您自己的操作系统和 shell 里核实。