上期结尾那句「确定性的事别派 agent 去做」欠了一笔账。官方给 hooks 的定义只有一句要紧:deterministic control: certain actions always happen rather than relying on the LLM to choose to run them。模型会忘、会跳过、会「觉得这次不用」;钩子不会。这期从一条能跑的 Prettier 配置起手,拆到官方 security-guidance 插件的 hook 注册表,再点名六个社区工具。
判断「该不该写成 hook」只有一个问题:这件事需要判断吗。需要判断 → 交给模型(prompt / skill / subagent)。不需要判断、每次都一样、漏一次就出事 → 钉成钩子。「我在 CLAUDE.md 里写了『改完代码一定要跑 format』」不是自动化,是祈祷——它进的是模型的 context,不是执行路径。本期顺序照旧:先给能抄的配置,再拆机制,再点名可装可读的条目,判据放后半程。
官方指南里逐字给了这段,放进项目根的 .claude/settings.json,Claude 每次用 Edit / Write 改完文件就自动跑 Prettier:
// .claude/settings.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }
]
}
]
}
}
事件数据是一段 JSON 打到 stdin,所以官方所有 Bash 示例都用 jq 取字段(brew install jq / apt-get install jq)。什么时候别写这条:如果文件也可能被 Bash 命令改写,Edit|Write 这个 matcher 就漏了——官方让你改用 FileChanged 钩子盯具体文件名。
写进 .claude/hooks/protect-files.sh(记得 chmod +x),在 PreToolUse 上注册成 "$CLAUDE_PROJECT_DIR"/.claude/hooks/protect-files.sh:
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# 命中就 exit 2:动作被拦下,stderr 作为反馈回给模型
case "$FILE_PATH" in
*.env|*/secrets/*) echo "该路径受保护,禁止写入" >&2; exit 2 ;;
esac
exit 0
/plugin install security-guidance@claude-plugins-official
# 提示 marketplace 找不到就先加:
/plugin marketplace add anthropics/claude-plugins-official
security-guidance 是官方唯一一个把自己整个建在 hooks 上的插件,装完什么都不用喊。它替你干掉的是:注入、不安全反序列化、dangerouslySetInnerHTML 这类问题从「等 PR review 抓」提前到「写下去的那一刻」。什么时候别装:它每个改了文件的 turn 会多一次模型调用、每次 commit 再多一次 agentic 审查(默认 Claude Opus 4.7),这是真金白银;不在 git 仓库里时后两层直接跳过;cloud session 不加载 plugin。嫌贵可以只留免费那层——ENABLE_CODE_SECURITY_REVIEW=0 关掉全部模型审查,保留纯字符串匹配的逐次编辑检查。
官方 reference 的事件表现在列了 33 个。社区文章里常见的「13 个生命周期事件」是旧的。几个容易被忽略但很好用的:
command — 跑 shell 命令。默认超时 10 分钟。http — POST 到一个 endpoint,body 里返回同样的 JSON。header 里的 $VAR 只解析 allowedEnvVars 列出的那些。mcp_tool — 直接调某个 MCP server 的工具。prompt — 把事件数据丢给一个模型(默认 Haiku)判 {"ok": true/false, "reason": "..."}。超时 30 秒。agent — 起一个 subagent,能读文件、跑命令再判。超时 60 秒、最多 50 轮工具调用。官方标记为实验性,生产工作流建议用 command。0 = 成功,stdout 按 JSON 解析(解析不了就当纯文本,部分事件会注入 Claude 上下文)。2 = 拦截,stderr 是拦截理由;能被拦的事件是有限几个(PreToolUse、UserPromptSubmit、Stop、SubagentStop、PreModelSwitch 等),SessionStart 这类拦不住,exit 2 只是把 stderr 显示给你。其余 = 非阻塞错误,动作照常进行,transcript 里出现一条 hook error。
/hooks 命令可以按事件浏览全部已配置钩子,但它只读——增删改要直接编辑 JSON。全局关闭是 "disableAllHooks": true;管理员那边还有个 allowManagedHooksOnly,能把你自己的钩子全部禁掉只留托管的。
security-guidance 把自己拆成五条注册,这张表值得当模板背下来(源码在 GitHub 上,目录里就是 .claude-plugin/ + hooks/ + tests/):
三件事可以直接学:贵的放后面(免费的字符串匹配放逐次编辑,花钱的模型审查放 Stop 和 commit);基线在前、判定在后(UserPromptSubmit 存快照,Stop 才 diff);审查者不是写代码那个实例(另起一次调用、全新 context)。
$CLAUDE_PROJECT_DIR 不跟着走。它永远指向会话启动时的项目根,所以 "$CLAUDE_PROJECT_DIR"/.claude/hooks/x.sh 跑的还是主 checkout 里那个脚本——这是对的。要知道 Claude 现在在哪个目录干活,读输入 JSON 里的 cwd 字段,它才跟着 worktree 和 cd 走。GowayLee/cchooks · Python SDK(132★)pip install cchooks # 或 uv add cchooks
from cchooks import create_context, PreToolUseContext
c = create_context()
assert isinstance(c, PreToolUseContext)
if c.tool_name == "Write" and ".env" in c.tool_input.get("file_path", ""):
c.output.exit_deny("Nope! .env files are protected")
else:
c.output.exit_success()
替你干掉:手写 json.load(sys.stdin) 和记退出码语义,事件类型是带类型的 context 对象。覆盖 9 种事件。什么时候别装:你的钩子就一行 jq | xargs——为它引一个包不划算;它那 9 种事件也远少于官方现在的 33 种,冷门事件得自己裸写。(据 GitHub 仓库页,2026-09)
syou6162/cchook · 把 JSON 换成 YAML(34★)go install github.com/syou6162/cchook@latest
PostToolUse:
- matcher: "Write|Edit"
conditions:
- type: file_extension
value: ".go"
actions:
- type: command
command: "gofmt -w {.tool_input.file_path}"
替你干掉:settings.json 里那坨转义地狱,条件(按扩展名、按模式)用声明式写,带 dry-run。什么时候别装:团队要共享配置时,多一层非官方格式就多一层「新人看不懂」;星数也说明它还是个小众工具。(据 GitHub 仓库页,2026-09)
johnlindquist/claude-hooks · TypeScript(393★)npx claude-hooks # 生成 .claude/hooks/index.ts,执行需要 Bun
替你干掉:payload 的类型提示与自动补全,全部事件写在一个 index.ts 里分发。什么时候别装:它把运行时依赖绑到了 Bun——CI 或队友机器上没有 Bun,钩子就静默失败(而 hook 失败默认不阻塞,你不开 debug 根本不知道)。(据 GitHub 仓库页,2026-09)
disler/claude-code-hooks-mastery · 教学仓(3.9k★)这个仓用 Astral uv 的单文件 Python 脚本把当时的每个事件都实现了一遍,含 PreCompact 前备份 transcript、拦截 subagent 完成这类花活。替你干掉:「这个事件的 JSON 长什么样」——照着读比翻文档快。什么时候别装:它是范本不是产品,README 自述覆盖 13 个事件,而官方 reference 现在是 33 个;整个 .claude 目录拷进你的项目会连它的 output styles、status line 一起带进来。(据 GitHub 仓库页,2026-09)
disler/claude-code-hooks-multi-agent-observability · 看板(1.5k★)它把所有事件通过 HTTP POST 打到一个 Bun 服务器,再 WebSocket 推到 Vue 客户端,多个并发 agent 的流向实时可见。替你干掉:「五个 agent 同时在跑,谁卡住了」这种睁眼瞎。什么时候别装:要 uv + Bun + 一个常驻服务器,单人单会话纯属过度工程;官方 http 类型钩子现在原生支持 POST 到 endpoint,自己接个日志服务可能比搬这套轻。(据 GitHub 仓库页,2026-09)
pre-commit/pre-commit · 不在 Claude 里的那一半(15.6k★)pre-commit(pip install pre-commit → pre-commit install,配置写在 .pre-commit-config.yaml)管的是 git 提交这一刻,和 Claude 的钩子不是替代关系是接力关系:Claude hook 只覆盖 Claude 干的事,你自己在终端敲的 commit 它一概看不见(官方明说 ! shell escape 里的 commit 也不算)。什么时候别装:只有你一个人、且所有改动都过 Claude——那多一层会重复跑同样的 linter。
command 钩子。需要但只看事件数据就能判 → prompt。要读代码才能判 → agent(且知道它是实验性的)。PostToolUse 补。代价是出事(写了 secret)→ PreToolUse + exit 2 拦在前面,因为 PostToolUse 撤不回来。hooks/hooks.json、skill frontmatter、subagent frontmatter 都能带钩子,装一个插件等于接受它注册的一切。claude --debug 或会话里 /debug。if 是尽力而为,不是安全边界。if: "Bash(rm *)" 这种过滤在 Claude Code 判不出命令结构时会照跑不误;官方直说:硬性的 allow/deny 要用权限系统,不是 hook。Edit|Write 抓不到 Bash 写的文件。要真全覆盖,官方给三条路:Stop 钩子每轮扫一次 git status --porcelain;或同时匹配 Bash|PowerShell;或用 FileChanged 盯文件本身。SessionEnd 的全部钩子共享 1.5 秒(显式写更长的 timeout 才会抬到最多 60 秒)——想在那儿做清理、上传日志的,先看这条。prompt 和 agent 两种 hook 类型,把你刚赶出去的不确定性又请了回来。它们有用(判断题本来就该模型判),但要认清楚——一个由 Haiku 判 ok: true/false 的 Stop 钩子不是确定性保障,它是在另一个地方多问了一次模型。真要「一定发生」,只有 command 那一档。先装这三个:① /plugin install security-guidance@claude-plugins-official——官方唯一整个建在 hooks 上的插件,装完即用,嫌贵就 ENABLE_CODE_SECURITY_REVIEW=0 只留免费那层。② 上面那段 Prettier 的 PostToolUse 配置(连同 jq)——五行 JSON,今天就能进 .claude/settings.json。③ pre-commit(.pre-commit-config.yaml)——补上 Claude 钩子照不到的那一半:你自己敲的 commit。
按需再看:Hooks reference(33 个事件全表、5 种类型字段、退出码语义)· security-guidance 源码(从 hook 里起一次独立模型调用再把结果喂回会话的完整写法)· cchooks(Python,消掉 stdin 样板)· cchook(YAML 配置 + dry-run)· claude-hooks(TypeScript 类型安全,要 Bun)· claude-code-hooks-mastery(各事件 JSON 长什么样,当字典查)· 多 agent 观测看板(并发 agent 的实时流向)。
明确别做:用 if 条件当安全边界(那是权限系统的活)· 指望 Edit|Write 抓到 Bash 写的文件 · 用 PostToolUse 拦本该拦在 PreToolUse 的事 · 装插件不看它注册了哪些钩子 · 在 SessionEnd 里做超过 1.5 秒的事 · 把 prompt/agent 钩子当成确定性保障。
Edit|Write、mcp__.*__write.*),SessionStart 里是 startup|resume|clear|compact|fork,FileChanged 里是字面文件名而非正则。PreToolUse、UserPromptSubmit、Stop 等)。additionalContext$CLAUDE_PROJECT_DIR vs cwdcd 走,用来知道现在在哪干活。{"ok": bool, "reason": str}。agent 类型官方标记为实验性。disableAllHooksallowManagedHooksOnly:只跑托管钩子,禁掉用户自己的。prompt 和 agent 两种要模型判断的类型?gofmt 还是问 Haiku,是第二个问题。所以这两个类型没有背叛设计,它们把「必然发生」和「必然正确」拆开了——只是用的人容易把它们当成一回事。security-guidance 用 Stop 钩子在后台跑审查,那模型岂不是可以一直被打断到跑不完?Stop 钩子这类「不许停」机制的通用难题——Claude Code 自己也在 Stop 钩子连续阻塞太多次时直接结束本轮并警告。凡是能让模型「继续」的钩子,都必须自带次数上限,否则你写的不是自动化,是一个自己给自己派活的永动机。/hooks 能列出全部已配置钩子及其来源文件),审查责任留给你。官方那句「安全是共同责任」不是免责声明式的客套——在这个设计里,唯一能判断「这个钩子该不该跑」的是知道自己项目里有什么的人。所以装插件前跑一次 /hooks、装完再跑一次看多出了什么,是这套机制里唯一真正属于你的那道关。SessionStart 配 compact matcher 重注入上下文、CwdChanged + FileChanged + CLAUDE_ENV_FILE、配置位置表、prompt/agent/http 三种类型、限制与排障(2026-09 访问)command/http/mcp_tool/prompt/agent 五种类型的字段;退出码 0/2/其它的完整语义与可阻塞事件清单;permissionDecision/additionalContext/updatedInput 等输出字段;Security considerations 一节;$CLAUDE_PROJECT_DIR 在 worktree 下的行为(2026-09 访问).claude/security-patterns.yaml 自定义规则表、五个 ENABLE_* 开关、末尾「How the plugin integrates with Claude Code」那张 hook 注册表(2026-09 访问).claude-plugin/ + hooks/ + tests/;想学「从 hook 里起一次独立模型调用、再把结果喂回会话」就读这个pip install cchooks,带类型的 context 对象与 exit_deny/exit_success,覆盖 9 种事件(据 GitHub 仓库页 132★,2026-09)go install github.com/syou6162/cchook@latest,用 YAML + {.tool_input.file_path} 模板替掉 settings.json 的转义地狱,带 dry-run(据 GitHub 仓库页 34★,2026-09)npx claude-hooks 生成 .claude/hooks/index.ts,强类型 payload;执行依赖 Bun、CLI 需 Node 18+(据 GitHub 仓库页 393★,2026-09)uv 单文件 Python 脚本,README 自述覆盖 13 个事件(官方现为 33 个,读时注意版本差),含 PreCompact 备份 transcript 等花活(据 GitHub 仓库页 3.9k★,2026-09).pre-commit-config.yaml;管的是提交这一刻,补上 Claude 钩子看不见的那一半(据 GitHub 仓库页 15.6k★,2026-09)