Claude Code Hooks 完全指南:用事件机制打造自动化编码 Agent 说实话我把 Claude Code 从“命令行问答工具”升级成“真正能放手的编码 Agent”靠的就是 Hooks。早期我用它改代码每次都要手动补一句“记得跑一下格式化”或“检查下有没有 lint 报错”改的文件一多这种重复指令极其消耗耐心而且容易漏。后来研究完 Hooks 机制把“改完文件自动跑格式化和 lint”“危险命令自动拦截”“每次会话自动归档日志”这类事情全部交给事件触发体验直接上了一个台阶。这篇是 Claude Code 系列指南的第四篇专门讲 Hooks。我会把事件类型、配置语法、匹配规则、四种 Hook 变体讲清楚然后用真实场景演示怎么落地。适合两类人一是已经会用 Claude Code 写代码、想把它接入工程化流程的人二是准备在团队里推广 Claude Code、需要统一安全和规范的人。读完你就能在自己的 settings.json 里写出第一套能用的 Hooks。1. Hooks 的核心机制与设计思路1.1 Hooks 本质上是一组“事件回调”用过前端的人对 addEventListener 不会陌生Hooks 的原理和它一模一样Claude Code 在运行过程中会触发各种生命周期事件你在配置里给这些事件挂上 shell 命令事件发生时系统就会执行对应脚本。举个例子Claude 要执行 Bash 工具之前会触发 PreToolUse 事件执行完文件编辑之后会触发 PostToolUse 事件你输入完 prompt 准备让 Claude 干活时会触发 UserPromptSubmit 事件Claude 回答完一轮、停下来等你输入时会触发 Stop 事件。每一个事件都会携带结构化 JSON 数据通过标准输入stdin传给你的脚本。你的脚本处理完数据后可以通过标准输出stdout和标准错误stderr反过来影响 Claude 的行为。比如 PreToolUse 事件的脚本发现命令里有危险操作就往 stdout 输出一个 BLOCK 标记Claude 的工具调用就会被拦住。这套设计最值钱的地方在于你不需要改 Claude Code 源码不用维护一个常驻服务只需要写一些可以被 shell 执行的脚本然后用 JSON 配置把它们挂到对应事件上。脚本可以是 Python、Node.js、Shell、Ruby甚至编译好的二进制只要系统能执行就行。1.2 事件模型完整拆解我把常用事件整理成了表格方便对照理解。事件类型触发时机典型用途PreToolUseClaude 调用任意工具之前危险命令拦截、权限审批、注入额外上下文PostToolUse工具执行完成后自动格式化、 lint、跑测试、记录工具执行结果UserPromptSubmit用户提交 prompt 之后、Claude 回复之前prompt 审计、敏感信息脱敏、团队规范校验StopClaude 完成一轮回复、等待用户输入时会话日志归档、状态同步、发通知SubagentStop子 Agent 完成任务返回时汇总子任务结果、检查子 Agent 产物NotificationClaude 需要用户授权或切换模式时桌面通知、Webhook 提醒PreCompact上下文压缩发生之前备份关键状态、导出摘要这里我重点说三个最容易出效果的。PreToolUse 是所有事件的“闸门”。因为它在工具真正执行之前触发你可以做到Bash 工具执行前检查命令内容发现rm -rf /或git push --force直接拒绝Read 工具读取敏感文件前做路径拦截WebSearch 执行前确认搜索词是否合规。拦截逻辑写在脚本里Claude 想绕都绕不过去因为它没有“跳过 hook”的权限。PostToolUse 是质量门禁。Claude 写完代码后紧接着触发格式化、lint、单测坏了就直接把报错信息反馈给 Claude 让它继续修。这样 AI 写代码和工程规范之间就有了闭环而不是每次写完再手动提要求。UserPromptSubmit 是最容易被忽略但收益极高的一个事件。它能在 Claude“看到”你的 prompt 之前先让你的脚本“看一眼”适合做日志、脱敏和规范检查。我会在第三部分专门演示。1.3 为什么不用手动脚本非要用 Hooks可能有人觉得这些事我手动跑不也行吗可以但实际工程里手动执行有四个致命问题。第一记忆不可靠。你让 Claude 改十个文件改到第五个的时候注意力早就分散了很容易漏跑检查。Hooks 是系统级触发只要配置好每次工具调用都会走一遍逻辑不会漏。第二策略不统一。团队里十个人用 Claude Code有人跑 lint 有人不跑代码风格很快会乱。Hooks 可以放进项目仓库的.claude/settings.json所有人在这个仓库里开发都自动生效。第三安全不可控。让 AI 随心所欲执行命令是有风险的手动监督在大规模并行编辑时会失灵。PreToolUse 拦截是最后一道防线相当于给 AI 上了一道“行为护栏”这比事后发现写坏了再回滚成本低得多。第四上下文碎片化。手动执行脚本的结果在终端里一闪而过没法自动回流给 Claude。但 PostToolUse 的 stdout 可以被 Claude 看到这意味着你可以在脚本里处理完数据后把关键信息“喂”给 Claude让它基于最新结果继续决策。2. 配置方法与参数详解2.1 配置文件的位置与优先级Hooks 写在 settings.json 里。Claude Code 会加载多层配置托管策略配置由企业管理员统一下发用户改不了适合强制安全基线用户级配置~/.claude/settings.json对你本机所有项目生效适合放个人偏好和全局日志项目级配置.claude/settings.json放在项目根目录下会随 Git 仓库走适合团队共享质量门禁和安全策略。配置合并的优先级通常是项目级 用户级 托管策略。具体值的覆盖关系不一定要死记你只需要记住一个原则和团队规范相关的放项目级个人习惯相关的放用户级强制底线放托管策略。如果你在 VS Code 里装了 Claude Code 插件Hooks 机制完全一样因为插件底层还是同一个 CLI读取的也是同一套 settings.json。所以不要去找什么 “VS Code 专用 Hooks 设置界面”没有直接改 JSON 就对了。2.2 Hooks 配置结构详解先看一个最基础的配置结构{ hooks: { PreToolUse: [ { matcher: Bash, hooks: [ { type: command, command: python3 scripts/check_command.py, timeout: 30 } ] } ] } }最外层是hooks对象key 是事件名。每个事件对应一个数组数组里每个对象都包含matcher和hooks两部分。matcher决定这个配置组匹配哪些工具或输入hooks是真正要执行的命令列表。hooks数组里的每一项都有两个必填字段type命令类型。默认写command其他还有commandAsync、commandBlock、commandAllow下一节细说。command要执行的 shell 命令字符串。可选字段是timeout单位秒默认 60 秒。超过这个时间hook 会被认为是失败或超时行为取决于事件类型。我个人强烈建议所有 hook 命令都写成“调用外部脚本文件”而不是直接在 JSON 里写一长串内联脚本。原因很简单JSON 里写复杂 shell 命令转义和换行很容易出错单独放一个scripts/目录可以用 Python 或 Node.js 写完整逻辑还能写单元测试。团队协作时脚本也能走代码评审。2.3 四种 Hook 变体Block、Allow、Async 和普通模式这是很多人一开始就绕晕的地方。同一个事件能挂的命令类型其实有区别关键在于这个事件是否需要“影响 Claude 的下一步行为”。普通command是阻断式的Claude Code 会等它执行完stdout 和 stderr 怎么处理取决于事件类型。而带后缀的类型会改变行为逻辑type 写法行为说明主要使用场景command普通阻塞执行按事件默认规则处理输出大部分通用场景commandBlock阻塞执行可阻止工具调用主要用于 PreToolUse危险命令拦截、权限审批commandAllow非阻塞执行只记录和观察不阻止工具调用审计、监控、指标采集commandAsync异步执行不等待结果不阻塞主流程日志上报、通知、耗时任务不同事件支持的类型还不一样。PreToolUse 支持command、commandBlock、commandAllow但不支持commandAsync因为它必须在工具执行前同步做出“允许还是阻止”的决策。PostToolUse 支持command、commandBlock、commandAsync可以用 Block 模式把 stdout 内容追加给 Claude也可以用 Async 模式异步做归档。UserPromptSubmit 支持command、commandBlock、commandAllow适合 prompt 审计和拦截。Stop、SubagentStop、Notification、PreCompact 都支持command、commandBlock、commandAsync通常用 Async 做收尾工作避免拖慢对话。这里的关键认知是Block 不一定比 Allow 好。Block 会拖慢主流程如果脚本写得慢用户体感就是“每次操作都卡一下”。所以我的习惯是需要拦截的就用 Block纯记录类的尽量用 Allow实在耗时的上报任务用 Async。2.4 matcher 匹配规则从全量匹配到精确匹配不写matcher时这个配置组会匹配该事件的所有触发场景。写matcher可以缩小范围避免无关工具也触发脚本。最简单的是工具名匹配。比如 PreToolUse 里只关心 Bash 工具就写matcher: Bash。注意工具名是大小写敏感的Bash 的 B 是大写Write、Edit、MultiEdit 同样首字母大写。多个工具可以用正则比如{ matcher: /^Edit$|^Write$|^MultiEdit$/, hooks: [ { type: command, command: python3 scripts/format_file.py } ] }带/ /包裹的就是正则表达式不包就是普通字符串匹配。更高级的用法是 JSONPath直接从 hook 输入 JSON 里做条件匹配。比如你想只在读取/etc/passwd时才拦截可以写{ matcher: $.tool_input.file_path, hooks: [ { type: commandBlock, command: python3 scripts/check_path.py } ] }不过 JSONPath 对新手不友好建议先从工具名匹配开始等确实遇到“需要根据参数内容区分”的需求再升级。2.5 Hook 脚本的输入输出约定每次 hook 被触发时系统会把事件 JSON 写入脚本的 stdin。不同事件的 JSON 结构不同但最核心的 PreToolUse 结构长这样{ tool_name: Bash, tool_input: { command: git status }, tool_response_id: xxxx }PostToolUse 会在后面多一个tool_response字段UserPromptSubmit 会带prompt字段。脚本要做的第一件事就是把 stdin 解析成 JSON然后取你关心的字段。输出约定需要特别注意对 PreToolUse 的 Block 型 hook如果脚本 stdout 以BLOCK开头工具调用会被阻止。普通输出不会阻止但脚本 stderr 的内容会被记录进会话Claude 能看到。对 PostToolUse 的 Block 型 hook脚本 stdout 的内容会随工具结果一起返回给 Claude 作为上下文。stderr 同样会传给 Claude。对 CommandAllow 这类观察型 hook输出不会影响主流程。这意味着脚本“话太多”会烧掉大量 token。PostToolUse 脚本如果每次编辑完都打印一屏日志Claude 每轮都要为这些日志消耗上下文。正确做法是成功时保持安静只在失败或需要补充关键信息时才输出。3. 核心场景实操从安全拦截到质量门禁3.1 实战一拦截危险命令给 Claude 上安全护栏我在团队里落地 Hooks 的第一件事就是拦住git push到主分支。因为 Claude 在自动修 bug 时可能会顺手把代码推到 main这在很多团队是不可接受的。先写一个检查脚本我放在.claude/scripts/guard_git_push.pyimport json import sys data json.load(sys.stdin) tool_name data.get(tool_name) tool_input data.get(tool_input, {}) if tool_name ! Bash: sys.exit(0) cmd tool_input.get(command, ) # 只关心 git push 相关命令 if git push not in cmd: sys.exit(0) # 判断是否推送到 main / master if any(branch in cmd for branch in [main, master]): print(BLOCK: 禁止直接推送 main/master 分支请使用 feature 分支并走 MR) print(已拦截对主分支的推送操作, filesys.stderr)然后在.claude/settings.json里挂载{ hooks: { PreToolUse: [ { matcher: Bash, hooks: [ { type: commandBlock, command: python3 .claude/scripts/guard_git_push.py } ] } ] } }注意这里用commandBlock因为我们需要在工具执行前把它拦截下来。脚本里优先用stdout输出BLOCK前缀系统会识别并阻止工具调用stderr里的解释会进入会话让 Claude 知道为什么被拦从而调整自己的行为。这套逻辑还可以扩展。比如拦截rm -rf指向项目根目录的命令、拦截对.env文件的 Read 操作、拦截curl上传源码到外部服务的命令。规则写多了以后Claude 在你这套环境里会自动学会避免触发这些警告相当于用代码约束了 Agent 的行为边界。3.2 实战二文件编辑后自动格式化和 lintPostToolUse 是我认为 Hooks 里“投产比”最高的事件。因为 Claude 改代码是高频操作每次写完自动做质量检查比事后统一跑一遍体验好得多。配置如下{ hooks: { PostToolUse: [ { matcher: /^Edit$|^Write$|^MultiEdit$/, hooks: [ { type: command, command: python3 .claude/scripts/quality_gate.py, timeout: 30 } ] } ] } }脚本核心逻辑import json import subprocess import sys data json.load(sys.stdin) tool_input data.get(tool_input, {}) file_path tool_input.get(file_path) if not file_path: sys.exit(0) # 仅处理当前项目内的文件 if not file_path.startswith(src/): sys.exit(0) # 先格式化再 lint subprocess.run([npx, prettier, --write, file_path], checkFalse) result subprocess.run([npx, eslint, file_path], capture_outputTrue, textTrue) if result.returncode ! 0: # 输出 lint 错误让 Claude 看到并修复 print(fLINT_FAIL: {file_path}\n{result.stdout[:2000]})这里面有个关键细节文件路径从tool_input.file_path取不要自己在命令里拼接变量。因为直接往 settings.json 里写prettier --write ${FILE_PATH}这类模板变量Claude Code 不会帮你展开最后会变成一个空的或错误的路径。把所有逻辑放到脚本里解析 JSON是最稳的做法。lint 失败时脚本输出LINT_FAIL前缀并附上错误摘要PostToolUse 的 stdout 会回流给 ClaudeClaude 会继续修复问题。这就形成了一个自动循环写文件 → 触发质量检查 → 有问题反馈给 Claude → Claude 修复 → 再触发检查直到通过。3.3 实战三UserPromptSubmit 做审计、脱敏和规范校验我在多个项目里部署过这个模式用户输入任何 prompt先经过钩子脚本写入本地日志顺便检查有没有把密钥贴进去。脚本如下import json import re import sys from datetime import datetime data json.load(sys.stdin) prompt data.get(prompt, ) # 1. 审计日志记录每次 prompt with open(.claude/prompt_log.jsonl, a) as f: f.write(json.dumps({ time: datetime.now().isoformat(), prompt: prompt[:500] }) \n) # 2. 敏感信息检测 secret_patterns [ rsk-[A-Za-z0-9]{20,}, rghp_[A-Za-z0-9]{20,}, rAKIA[A-Z0-9]{16}, ] for pattern in secret_patterns: if re.search(pattern, prompt): print(BLOCK: 检测到疑似密钥内容请输入经过脱敏的 prompt) print(请移除密钥后再发送, filesys.stderr) sys.exit(0)这个脚本的效果立竿见影。之前有同事在 prompt 里贴了线上数据库连接串Claude 直接把连接串写进代码里再提交差点出事。加了 UserPromptSubmit 拦截之后密钥根本进不到 Claude 上下文里。还有团队拿它做规范校验要求 prompt 必须带上需求单号没有就通过 stderr 提醒。这比人工 review 聊天记录靠谱多了因为拦截发生在模型看到内容之前属于前置约束。3.4 实战四Stop 事件做会话归档与异步通知Stop 事件在 Claude 回复完之后触发适合做“每一轮对话的落盘”。我个人的习惯是用commandAsync把归档任务丢到后台不阻塞下一轮输入。{ hooks: { Stop: [ { hooks: [ { type: commandAsync, command: python3 .claude/scripts/archive_session.py } ] } ] } }归档脚本做的事情很简单把当前会话的关键消息追加到session_archive.jsonl。这个文件的用途是之后做数据复盘比如统计这个项目里 Claude 花了多少轮才解决问题、哪些类型的修改频繁触发 lint 错误、有没有反复修改同一个文件。不要小看这个日志有一次我排查“为什么某天 Claude 突然一直改错文件”就是从 Stop hook 的归档日志里发现上午我在 prompt 里给了一个过时的目录结构Claude 后续所有判断都基于那个错误上下文。有了历史日志这类问题就变成可追溯的了。3.5 实战五与本地服务和其他 CLI 工具联动Hooks 本质就是执行 shell 命令所以它能联动的对象远超 Claude Code 本身。你可以让它在文件更新后调用本地 Flutter/Django 的格式化工具可以把工具调用记录推送给内部审计 API也可以把 Claude 的修改摘要交给本地模型服务做二次校验或者生成变更说明。一个比较实用的做法是在 PostToolUse 里把“改动的文件列表”交给一个本地脚本脚本自动生成 commit message 草案并追加到临时文件。这样当 Claude 准备提交代码时它能直接引用一份结构化的变更说明提交信息质量有明显提升。再比如很多团队会写内部 CLI 来做代码规范校验你可以直接把它作为 hook 挂在 PostToolUse 上命令换成my-cli check $(jq -r .tool_input.file_path $HOOK_INPUT)。注意这里如果要用 jq系统里必须有 jq而且 Hook 执行环境和终端环境不一定完全相同这在第四部分排查里我会重点提醒。3.6 大型代码库中落地 Hooks 的三个原则在超大仓库里用 Hooks最重要的一点是不要让钩子脚本变成性能瓶颈。PreToolUse 是同步阻塞的如果脚本逻辑复杂每次 Claude 调用工具都有明显延迟整个体验会变得不可用。我的三条实操原则第一拦截逻辑保持轻量。PreToolUse 脚本只做模式匹配和判断不要做深度分析。判断命令里有没有危险关键字用 Python 的in或正则就够别在拦截脚本里做 AST 解析。第二重量操作异步化。格式化、日志归档这类任务能挂 Async 就别用同步 Block。格式化可以放到 PostToolUse 的普通模式里日志就放到 Stop 的commandAsync。第三用git diff限定范围。PostToolUse 触发时不要对整个仓库跑 lint只针对tool_input.file_path或者git diff --name-only输出的文件列表。很多团队在 CI 里已经这么做了Hooks 里同理否则改一个文件触发两分钟全仓扫描谁也受不了。4. 常见问题与排查技巧实录4.1 Hook 没触发先查这三件事我遇到过最多的问题是配置写了脚本也挂了但事件就是不触发。排查顺序如下第一确认配置文件路径。你改了~/.claude/settings.json但当前项目里存在.claude/settings.json项目级配置覆盖或合并后把优先级顶掉了导致你以为生效的规则其实没生效。建议检查时先claude里执行hooks相关的帮助命令或者直接手动打开两个文件看有没有冲突。第二确认 matcher 有没有匹配上。写matcher: Bash时如果实际触发的是Bash(git status)这种带额外信息的工具名就匹配不上。同理正则写错了也会静默失败。最简单的验证方式是在脚本第一行把收到的事件 JSON 写到一个临时文件然后手动构造一次触发看 JSON 长什么样。第三确认脚本本身能不能在 Claude Code 的环境里跑通。Claude Code 在 GUI 里启动时PATH 环境变量可能和终端里不一样python命令找不到 Python、node找不到 Node 就会静默失败。这时候不要用python直接用绝对路径比如/usr/bin/python3或者用which python3查一下再写死。4.2 Timeout 和异步执行的坑默认情况下 hook 有 60 秒超时。看起来很长但 PreToolUse 是阻塞式的一旦脚本执行时间超过预期Claude 的工具调用会被挂起表现就是“卡住不动”。如果你的脚本里跑了npm install或全仓eslint60 秒很容易超。我踩过的坑是这样的第一次写 PostToolUse 格式化脚本时直接在命令里写了npx prettier --write结果 npx 在第一次运行时要下载包光下载就等了十几秒。更麻烦的是这个下载消耗不是每次都有于是脚本表现不稳定有时快有时慢。解决办法是项目里固定依赖本地安装好的prettier/eslint命令写node_modules/.bin/prettier而不是npx prettier。或者干脆在脚本开头对依赖做一次确定性检查缺依赖直接返回不要现场下载。异步 hook 也有坑。commandAsync虽然不阻塞主流程但它不会像同步 hook 那样捕获 stdout 回传日志写在哪里需要你自己控制。而且异步脚本并发执行时如果都往同一个文件追加内容可能产生并发写问题。解决办法是在脚本里用追加模式打开文件或者加一个简单的文件锁。4.3 stdout 和 stderr 输出污染上下文这个问题非常隐蔽。PostToolUse 的 Block 型 hook 会把 stdout 内容作为上下文发给 Claude如果你的脚本习惯性打印了成功信息比如Formatting complete.、Processed 12 files.每一轮都会被塞进上下文。测试跑下来你会觉得很奇怪明明没干多少活怎么上下文消耗这么快查了半天发现是 hook 脚本刷屏。所以 PostToolUse 脚本的纪律是正常情况不输出异常情况才输出而且输出要精炼。对于 PreToolUse 的 stderr 也一样。Claude 能看到 stderr如果你把脚本内部调试信息全打进去Claude 可能会被干扰以为发生了什么错误。建议脚本里不要用print做日志追踪真要记录状态就写文件。4.4 权限与路径问题Hooks 是以当前用户的权限执行的不会额外提权。如果你的脚本需要写/etc下的文件或者需要访问某个只有 root 才能读的目录它不会成功。另一个典型问题是工作目录。Claude Code 的主进程通常以项目根目录作为 cwd你写的脚本路径和相对路径都要基于这个理解。比如command: python3 .claude/scripts/check.py里的.claude是相对于项目根目录的。但要是你在脚本里还写了open(log.txt, w)这个log.txt也会写到项目根目录下而不是脚本所在目录。需要根据实际需求使用绝对路径或通过环境变量判断当前目录。Windows 环境下还要注意 shell 差异。settings.json 里的 command 在 Windows 上走的是 cmd 的语法和 Linux/macOS 的 bash 语法有区别。跨平台团队的建议是把脚本用 Python 写命令统一成python3 .claude/scripts/xxx.py尽量避免在 JSON 里直接写 shell 专属语法。4.5 高频问题速查表现象可能原因快速解法Hook 完全不触发配置文件路径不对或 matcher 未匹配检查项目级和用户级 settings.json临时把 matcher 删掉测试工具调用经常卡住同步 hook 执行过慢优化脚本改用绝对路径解释器去掉 npx 下载逻辑上下文消耗异常高PostToolUse 脚本 stdout 刷屏脚本只在异常时输出成功保持静默拦截不生效用了 command 而不是 commandBlock确认事件是否支持拦截改成 commandBlock异步日志丢失commandAsync 不捕获输出日志内容直接由脚本写文件不要依赖 stdoutWindows 下脚本报错shell 语法不兼容统一用 Python 脚本避免 bash 专属指令4.6 调试 Hooks 的三个技巧最后分享三个实战调试技巧。第一写一个 dump 脚本。把输入 JSON 原封不动写到文件。不确定某个事件携带什么数据结构时就先用 dump 脚本跑一轮看完输出再写正式逻辑。import json, sys with open(/tmp/hook_input.json, w) as f: json.dump(json.load(sys.stdin), f, ensure_asciiFalse, indent2)第二在命令里临时加个标记文件。有些 hook 执行太快你不知道它到底有没有跑到。在脚本入口处写一个touch /tmp/hook_running.flag然后手动触发工具调用看这个文件有没有生成。没有生成就是脚本没被执行生成就是脚本内部逻辑有问题。第三用 exit code 做故障隔离。脚本里捕获所有异常异常时sys.exit(2)并往 stderr 写信息正常路径sys.exit(0)。这样当你看到工具被莫名卡住或拦截时可以快速判断是脚本抛异常还是业务规则触发。我个人在实际操作中的体会是Hooks 这个功能真正拉开差距的不是“会用”而是“克制”。你不需要在第一天上手就挂十个事件那样只会让自己疲于调试。先把 UserPromptSubmit 的审计日志跑起来再补一个 PreToolUse 危险命令拦截最后把 PostToolUse 的格式化和 lint 加上三步走完已经能覆盖绝大部分工程化需求。踩过几次坑之后我现在最推荐的第一次尝试是从 UserPromptSubmit 日志开始。把每天的 prompt 落到一个 JSONL 文件里第二天回看你会很直观地发现自己在让 Claude 重复做哪些事然后才能真正理解该用哪个事件去自动化掉它们。