write-hook

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Claude Code hooks

Claude Code hooks

Behaviours observed in Claude Code web sessions, 2026-08-22 and 2026-08-23 - not inferred from docs.
2026-08-22 和 2026-08-23 在 Claude Code Web 会话中观察到的行为——并非根据文档推断。

Injecting context: the mechanism differs per event

注入上下文:不同事件采用的机制不同

EventHow its output reaches the model
SessionStart
,
UserPromptSubmit
stdout on exit 0 is added to context as-is
Stop
stdout is NOT. Emit JSON with
hookSpecificOutput.additionalContext
Plain text on stdout from a
Stop
hook is the most common way to ship a hook that silently does nothing: the script runs, the text goes to the debug log, the model never sees it.
json
{
  "hookSpecificOutput": {
    "hookEventName": "Stop",
    "additionalContext": "..."
  }
}
事件其输出如何到达模型
SessionStart
、
UserPromptSubmit
退出码为 0 时,stdout 会原样添加到上下文中
Stop
stdout 不会到达模型。需要输出带有
hookSpecificOutput.additionalContext
的 JSON
Stop
hook 在 stdout 上输出纯文本,是导致 hook 悄无声息地不起作用的最常见方式:脚本会运行,文本会进入调试日志,但模型永远看不到它。
json
{
  "hookSpecificOutput": {
    "hookEventName": "Stop",
    "additionalContext": "..."
  }
}

A
Stop
hook that changes what happens next MUST guard on
stop_hook_active

会改变后续行为的
Stop
hook 必须以
stop_hook_active
作为防护依据

Injecting context from
Stop
does not just add text - it restarts the turn, which fires
Stop
again. Observed: four consecutive turns with no user input, burning tokens.
stop_hook_active
is a field of the JSON the hook receives on stdin, true as soon as the current turn has already been restarted by this hook. Read stdin, exit 0 emitting nothing when it is true.
bash
INPUT=$(cat)
if printf '%s' "$INPUT" | grep -Eq '"stop_hook_active"[[:space:]]*:[[:space:]]*true'; then
  exit 0
fi
从
Stop
注入上下文不只是添加文本——它还会重启当前轮次,从而再次触发
Stop
。曾观察到:在没有用户输入的情况下连续执行四轮,消耗 token。
stop_hook_active
是 hook 通过 stdin 接收的 JSON 中的一个字段;一旦当前轮次已经被此 hook 重启,该字段就会变为 true。读取 stdin;当它的值为 true 时,以状态码 0 退出且不输出任何内容。
bash
INPUT=$(cat)
if printf '%s' "$INPUT" | grep -Eq '"stop_hook_active"[[:space:]]*:[[:space:]]*true'; then
  exit 0
fi

Scope of the rule

规则的适用范围

  • Applies to
    Stop
    , because the event can re-fire itself. Whatever the hook emits to change what happens next, the guard applies - the trigger is the event, not the output channel.
  • Does NOT apply to
    SessionStart
    or
    UserPromptSubmit
    . Adding the check there is cargo cult.
  • A
    Stop
    hook that only reads state (logs, metrics) and emits nothing needs no guard.
  • 适用于
    Stop
    ,因为该事件可能再次触发自身。无论 hook 输出什么来改变后续行为,防护规则都适用——触发因素是事件本身,而不是输出通道。
  • 不适用于
    SessionStart
    或
    UserPromptSubmit
    。在这些事件中添加这项检查属于 cargo cult 式盲目照搬。
  • Stop
    hook 如果只读取状态(日志、指标)且不输出任何内容,则不需要防护。

Nothing substitutes for the guard

没有任何机制可以替代该防护

stop_hook_active
is the only signal the harness itself sets, scoped to exactly the turn being restarted. Add it first; any further dedupe sits on top of it, never in its place.
Substitute seen in reviewWhy it is not one
Marker file, once-per-session flagNot turn-scoped. Silences the hook for the rest of the session, and a stale or wiped file changes behaviour
Matching the reminder text in
last_assistant_message
Depends on the model repeating a string. A guard that the model can decline to honour is not a guard
"It exits 0 / never blocks / is idempotent"Describes the exit path, not the re-fire. See the table below
stop_hook_active
是 harness 自身设置的唯一信号,其作用域严格限定为正在被重启的那个轮次。应优先添加它;其他任何去重逻辑只能建立在其上,绝不能取代它。
审查中出现的替代方案为何不算替代方案
标记文件、每会话仅执行一次的标志并非按轮次限定作用域。它会让 hook 在会话余下时间中不再执行,而文件过期或被清除也会改变行为。
在
last_assistant_message
中匹配提醒文本
取决于模型是否重复某个字符串。一个可能被模型无视的防护并不是真正的防护。
“它以 0 退出/从不阻塞/具备幂等性”描述的是退出路径,而不是重复触发问题。参见下表。

Rationalizations

常见理由

ExcuseReality
"
stop_hook_active
only matters for hooks that BLOCK the stop - ours never blocks, there is nothing to recurse on"
This is the reasoning that produced the four-turn cascade. Injecting context restarts the turn on its own; blocking is a separate mechanism
"My dedupe is strictly stronger than
stop_hook_active
"
Stronger at something else. It is session-scoped, not turn-scoped, and it lives outside the harness
"Add the guard after the demo / after the release"Four lines. The cascade fires on the first live turn, in front of whoever is watching
"The reviewer said it is cargo cult"Ask them for a
Stop
hook that emits
additionalContext
, has no guard, and provably cannot re-fire. There is none
借口实际情况
“
stop_hook_active
只对会 BLOCK 停止过程的 hook 有影响——我们的 hook 从不阻塞,因此不存在递归问题”
正是这种思路引发了连续四轮的级联重复。注入上下文本身就会重启当前轮次;阻塞是另一套独立机制。
“我的去重逻辑严格强于
stop_hook_active
”
它只是在另一个方面更强。它按会话限定作用域,而不是按轮次,而且位于 harness 之外。
“演示之后/发布之后再添加防护”只需要四行代码。级联重复会在第一次实际运行的轮次中当场触发,并出现在任何观察者眼前。
“审查者说这是 cargo cult”要求他们提供一个会输出
additionalContext
、不带防护,并且能证明不会再次触发的
Stop
hook。并不存在这样的 hook。

Red flags - STOP

危险信号——STOP

  • A
    Stop
    hook that emits output but whose script never reads stdin.
  • The words "non-blocking", "idempotent" or "only fires once" offered as the reason no guard is needed.
  • A guard that greps the model's own output instead of the hook's input.
  • Shipping now, guarding later.
  • Stop
    hook 会输出内容,但其脚本从不读取 stdin。
  • 把“non-blocking”“idempotent”或“only fires once”等说法当作无需防护的理由。
  • 防护逻辑对模型自己的输出进行 grep,而不是读取 hook 的输入。
  • 现在先发布,之后再补防护。

Cloud sessions

云端会话

FactConsequence
Hooks in a repo's
.claude/settings.json
run; those in
~/.claude/settings.json
are ignored
Commit the hook, or it does not exist in a web session
SessionEnd
is unreliable - the container is reclaimed after inactivity, and its matchers are terminal-shaped
Never make persistence depend on it. Commit and push as work is produced
Stop
fires when Claude finishes responding, and receives
last_assistant_message
The only dependable per-turn hook point
事实影响
仓库
.claude/settings.json
中的 hook 会运行,而
~/.claude/settings.json
中的 hook 会被忽略
请提交该 hook,否则它在 Web 会话中并不存在。
SessionEnd
不可靠——容器会在闲置后被回收,而且其 matchers 面向终端事件
绝不要让持久化依赖它。每完成一项工作就提交并推送。
Stop
会在 Claude 完成响应时触发,并接收
last_assistant_message
唯一可靠的按轮次 hook 触发点

Before shipping a hook

发布 hook 之前

  1. Feed it the JSON it will receive on stdin, for each branch - field absent, false, true.
  2. Check the exit code and the exact bytes on stdout.
  3. For a
    Stop
    hook, confirm the guard path emits nothing.
  1. 针对每个分支,向它输入实际会从 stdin 收到的 JSON——分别覆盖字段缺失、false 和 true 的情况。
  2. 检查退出码和 stdout 上的确切字节。
  3. 对于
    Stop
    hook,确认防护路径不会输出任何内容。