
最近把日常开发里的重复劳动外包给 Claude Code 之后我发现一个很有意思的问题AI 干起活来确实快但它不会主动“找你汇报”。一个长任务跑着跑着可能早就停下来等确认了我却还盯着屏幕发呆一次会话结束日志没存、改动没提交下一次又得重新交代一遍。与其说是 Agent 不够聪明不如说是缺了一套“事件通知”机制。这套机制在 Claude Code 里叫 Hooks——它不改变模型本身而是在 Agent 生命周期里的关键节点上挂脚本让整个工作流自动串起来。这篇是 Claude Code 完整指南的第四篇假设你已经装好环境、跑过基础会话。如果还比较生疏前几篇讲过安装、配置和基础命令先补上再来看本文效果更好。Hooks 适合两类人一类是想把 Claude Code 接进自己工作流的开发者另一类是团队里希望统一 AI 编码规范的负责人。读完你会有能力自己设计一套“事件驱动式”的自动化管线。1. Hooks 到底在自动干什么从“人盯着”到“事件驱动”1.1 没有 Hooks 时你得手动做多少事先说说我自己没接 Hooks 之前的状态。当时让 Claude Code 帮我重构一个服务模块任务很长运行到一半它停下来问我某个接口要不要改。我人没在电脑前回来一看它已经等了好几分钟白白浪费了时间。还有更琐碎的每次开新会话要手动把项目背景、技术栈、目录结构贴一遍跑了二三十轮之后上下文变长经常忘了之前讨论过什么决策Claude Code 执行Edit改完代码没有自动跑 lint编译到一半才发现风格问题会话结束终端一关之前的对话记录、临时脚本、改动历史全都散落各處这些事的本质是Claude Code 本身是一个事件循环agent loop每一步都在“思考—调用工具—处理结果—再思考”但这个循环默认是封闭的。作为一个外部程序你很难知道它此刻进行到了哪一步更别说在特定时刻让外部逻辑介入。Hooks 就是把“封闭循环”打开一排窗口:在这些窗口上挂上你的脚本让每个关键环节都能触发外部动作。1.2 Hooks 解决问题的核心思路Hooks 的思路和生活里装感应灯类似。正常人不会进屋之前先手动计算时间、再按开关而是让“人进门”这个事件自动触发“灯亮”。Claude Code 也一样你不用每轮追问“你执行到哪了”而是在“用户提交提示词”“调用工具之前”“工具执行之后”“会话即将停止”等节点上挂脚本让脚本自动记录、校验、通知、存档。关键点是Hooks 执行的是完全独立的脚本不依赖模型本身。模型可能被换掉工具名称可能更新但只要是 Claude Code 这个客户端在跑Hooks 照样生效。这意味着你可以把团队规范、安全检查、日志备份这些逻辑和模型能力彻底解耦换模型不影响自动化工作流。2. 事件触发地图九个事件分别卡在 Agent 流水线的哪个节点要使用 Hooks第一步不是写脚本而是搞清楚 Claude Code 到底暴露了哪些事件。以我目前使用的版本为例常见的事件有九个可以分为生命周期、交互与工具、边缘事件三类。2.1 生命周期三段式SessionStart / Stop / SessionEndSessionStart 在每次新会话开始、Claude Code 准备加载上下文时触发。这个节点适合注入项目背景、加载常用指令、打印环境信息。它有个特殊能力你可以返回结构化 JSON把一段自定义内容直接注入到模型上下文的最前面。后面实战部分会展开。Stop 是会话即将停止时触发的事件。这里说的“停止”指模型认为任务已结束或者因为你明确要求它停下来。注意它和 SessionEnd 不一样Stop 时会话还活着你还有机会让 Claude Code 继续干点别的事SessionEnd 则是进程真正退出前的最后一站适合做清理、备份、发送通知。SessionEnd 触发时你已经基本“出不了手”了所以别指望在这里再让 Claude Code 执行多复杂的操作。我的习惯是Stop 里做“还能补救”的事情比如自动提交代码SessionEnd 里只做日志归档和提醒。2.2 交互与工具回调UserPromptSubmit / PreToolUse / PostToolUse这三个是日常最常用的它们卡在模型工作循环的核心位置上。UserPromptSubmit 在用户提交提示词时触发不管是手动敲的还是从文件读的。它拿得到用户这次的完整输入可以在提示词进入模型之前做记录、改写、拦截。非常适合做“提问审计”也可以设定关键词规则比如发现提示词里包含“删除数据库”之类的危险字眼就拒绝继续。PreToolUse 在任一工具被调用之前触发范围包括 Bash、Edit、Read、Write、WebFetch 等所有工具。这是安全检查的最佳位置你可以在 Claude Code 真正执行rm -rf或git push之前让脚本对命令字符串做正则匹配命中危险模式就返回“拒绝”。PostToolUse 在工具执行完成后触发。此时脚本能拿到工具名和工具输入但拿不到工具的输出内容。它适合做“执行后校验”比如每次Edit完代码就自动跑一下语法检查。如果校验失败Claude Code 会把失败信息反馈给模型让它自己尝试修复。2.3 边缘事件Notification / SubagentStop / PermissionRequestNotification 在 Claude Code 需要用户注意时触发典型场景是需要人工批准权限。你可以在自己的脚本里调用系统通知、发到企业微信机器人或者飞书 Webhook远程也能第一时间知道“它卡在审批上了”。SubagentStop 在子代理subagent完成自己的任务时触发。如果你的工作流里经常派多个子代理并行调研这个事件可以用来做汇总通知。PermissionRequest 是较新版本里出现的它发生在 Claude Code 感觉自己需要额外权限、向用户发起请求的时候脚本可以在这一层自动化处理部分权限请求减少人工干预。这九个事件不是每个项目都全用。我自己的建议是先看你的痛点是“上下文记忆”“安全检查”还是“结束归档”从中挑一个事件开始全用反而容易被脚本拖累。事件触发时机典型用途SessionStart新会话开始注入上下文、加载环境UserPromptSubmit用户提交提示词记录、改写、拦截PreToolUse工具被调用前安全检查、自动授权PostToolUse工具执行完毕后校验、格式化、触发下游Stop会话即将停止自动提交、续跑判断SessionEnd进程退出前归档、备份、通知Notification需要用户注意时桌面/IM 通知SubagentStop子代理完成汇总结果PermissionRequest发起权限请求时自动审批部分请求3. settings.json 是启动台位置、结构与 matcher 匹配逻辑3.1 配置文件的位置与生效范围Claude Code 的 Hooks 配置统一写在 settings.json 里没有独立的 hooks 文件这是很多人一开始找不到入口的原因。它有两个层级用户级配置~/.claude/settings.json全局生效适合放通用自动化逻辑项目级配置.claude/settings.json项目根目录下跟随仓库走适合放团队规范和安全策略两级配置会合并同名的 hooks 事件会叠加执行。我接触过不少团队最终都采用“用户级放个人效率脚本、项目级放团队合规脚本”的分工方式。这样个人可以在自己的机器上随意调 hooks而团队规范跟着仓库走换电脑也不丢。3.2 matcher 的匹配逻辑每个事件下面的配置是一个数组数组元素叫“匹配器”。写 Hooks 配置时很多人会困惑 matcher 到底匹配什么。答案取决于事件类型PreToolUse 和 PostToolUse 的 matcher 匹配工具名比如Bash、Edit|Write支持正则UserPromptSubmit 的 matcher 匹配用户提交的文本内容SessionStart、SessionEnd、Stop 这类生命周期事件通常没有可匹配的对象matcher 留空字符串要强调一点matcher 为空字符串时表示匹配全部。很多示例里写着matcher: 意思就是“这个事件的所有情况都触发下面的 hooks”。3.3 第一份最小配置下面是一个最小可用的配置示例我通常拿它当“脚手架”{ hooks: { SessionStart: [ { matcher: , hooks: [ { type: command, command: echo \[hooks] session started\ } ] } ], UserPromptSubmit: [ { matcher: , hooks: [ { type: command, command: python3 ~/.claude/scripts/log_prompt.py, timeout: 10 } ] } ] } }这里有几个细节值得留意。command的写法就是把要执行的 shell 命令完整写进去和你在终端里敲的命令一致。timeout字段是超时秒数建议显式设置尤其当你不想让脚本卡住整个 Agent 的时候。脚本路径建议用绝对路径因为 Hooks 执行时的当前目录未必是你以为的那个项目目录。配置修改后重启 Claude Code 会话就会生效。不需要重新安装也不需要claude reset这是 Hooks 的一个优势它本质上是外部配置和模型状态无关。4. 让外部脚本和 Claude Code 对话退出码、JSON 输出与传参很多人第一次写 hook 脚本以为只要把命令挂上去脚本输出就会被 Claude 看到。实际上没那么简单这里有一套明确的“通信协议”。理解不了这套协议写出来的 Hooks 就是时灵时不灵的玄学。4.1 退出码阻断与报错的判定hook 脚本执行完毕后的退出码exit code具有特殊含义退出码 0脚本执行成功Claude Code 继续走正常流程非 0 退出码脚本执行失败会被 Cloude Code 记录在 PreToolUse 等“前奏事件”里非 0 还会直接阻断对应工具的执行所以如果你想拦截一个危险命令最简单粗暴的写法就是sh 脚本里判断命令字符串命中危险模式就直接exit 1什么都不用输出工具就会被阻止。这个机制特别适合“一刀切”的安全需求。4.2 JSON 输出结构化指令退出码只是“交通信号灯”JSON 输出才是 Hooks 真正的高级玩法。当 hook 脚本向标准输出打印一段特定格式的 JSON 时Claude Code 会解析它并根据hookSpecificOutputType做不同的处理。下面是几个核心类型输出类型适用事件作用contextOnStartSessionStart注入一段内容到上下文开头userPromptSubmitUserPromptSubmit在用户提示词之外附加额外内容permissionDecisionPreToolUse直接允许或拒绝工具调用stopHookModeStop告诉 Claude Code 继续运行不停止比如 PreToolUse 里自动放行某个工具的脚本可以输出这样的 JSON{ hookSpecificOutput: { hookSpecificOutputType: permissionDecision, permissionDecision: allow } }这个机制把我第一次见的时候惊艳到了因为它等于把 Claude Code 的权限系统整个对外开放了。原来每次执行 Bash 都可能弹权限确认通过 Hooks 可以针对特定命令自动允许不用再一轮轮点确认。但这里要泼一盆冷水自动 allow 权限能力很强大、也很危险千万别对Bash这类高危工具做无差别放行。我见过有人图省事写了个matcher: Bash的自动 allow结果 Claude Code 跑出一条rm -rf居然直接执行了幸亏是测试目录。4.3 给 Hook 传参数stdin 里的 JSONhook 脚本启动时Claude Code 会通过标准输入注入一段 JSON包含这个事件的上下文信息。包括会话 ID、当前工作目录、触发的事件名、工具名、工具输入等。比如 UserPromptSubmit 事件会给你prompt字段PreToolUse 事件会给你tool_name和tool_input。你可以在脚本里用sys.stdin.read()读入这段 JSON然后用 Python 或 jq 提取字段。这就是手机支架和仪表盘的区别会解析 stdin 的 hook 脚本可以根据上下文做出不同决策而不只是无脑 echo。5. 五个拿来即用的自动化场景配置理论部分说完了下面分享五个我自己在项目里实际跑着的场景配置。每个都给出完整思路和关键代码你可以对照自己的项目改。5.1 场景一SessionStart 自动注入项目上下文这个场景解决我开头说的“每次都要重复贴背景”的痛点。我用一个 Python 脚本从项目根目录读PROJECT_CONTEXT.md如果文件存在就把内容通过contextOnStart注入会话开头。#!/usr/bin/env python3 import json import pathlib ctx_file pathlib.Path(PROJECT_CONTEXT.md) if not ctx_file.exists(): sys.exit(0) context ctx_file.read_text() print(json.dumps({ hookSpecificOutput: { hookSpecificOutputType: contextOnStart, context: context } }))配置文件里挂到 SessionStart 即可{ hooks: { SessionStart: [ { matcher: , hooks: [{ type: command, command: python3 /absolute/path/inject_context.py, timeout: 10 }] } ] } }这个方案的好处是上下文内容完全由你掌控。改PROJECT_CONTEXT.md不删任何模型知识。团队场景下可以把这个文件提交到仓库新成员一进项目Claude Code 自动就懂项目背景新人引导成本直线下降。5.2 场景二UserPromptSubmit 记录提问流水做审计和复盘时我希望能翻看“之前跟 Claude 提过哪些需求”。在 UserPromptSubmit 上挂一个记录脚本把每次用户输入按时间戳追加到本地日志。#!/usr/bin/env python3 import json import sys import time import os data json.load(sys.stdin) prompt data.get(prompt, ) log_file os.path.expanduser(~/.claude/logs/prompt_history.log) os.makedirs(os.path.dirname(log_file), exist_okTrue) with open(log_file, a, encodingutf-8) as f: f.write(f[{time.strftime(%Y-%m-%d %H:%M:%S)}] {prompt}\n)这个脚本不需要任何 JSON 输出纯粹是“观察者模式”。它的价值在于让你事后能完整还原哪天、哪个项目、提了什么需求、Claude 是怎么一步步处理的。结合会话日志基本能做到百分百可回溯。5.3 场景三PreToolUse 做 Bash 安全闸门这是最有“守护神”气质的一个。我在 PreToolUse 事件里匹配Bash工具用脚本检查命令字符串。如果识别出危险模式就输出permissionDecision: deny阻止执行。#!/usr/bin/env python3 import json import re import sys data json.load(sys.stdin) tool_name data.get(tool_name, ) if tool_name ! Bash: sys.exit(0) command data.get(tool_input, {}).get(command, ) danger_patterns [ r\brm\s-rf\s/, # 删除根目录 rmkfs\b, # 格式化磁盘 r:\(\)\s*\{\s*:\|:\s*\};:, # fork 炸弹 ] for pattern in danger_patterns: if re.search(pattern, command): print(json.dumps({ hookSpecificOutput: { hookSpecificOutputType: permissionDecision, permissionDecision: deny, reason: f命令命中危险模式: {pattern} } })) sys.exit(0)注意看这里判断命中后是sys.exit(0)而不是exit 1。因为我们已经用 JSON 输出了明确的“拒绝”决策退出码不用再表示失败。如果你用exit 1Claude Code 会把它当成脚本错误而不是工具拒绝语义上就容易混乱。我的经验是能用 JSON 表达的业务决策就走 JSON退出码只留给“脚本自己崩了”的情况。5.4 场景四PostToolUse 改完代码自动跑 lint这个场景是“少操心”的关键。每次 Claude Code 完成Edit或Write操作我们的 PostToolUse hook 就会自动跑一次项目的 lint 命令。如果 lint 失败Claude Code 会把报错内容反馈给模型模型会立刻尝试修复形成一个小型的“改代码—校验—修复”闭环。{ hooks: { PostToolUse: [ { matcher: Edit|MultiEdit|Write, hooks: [ { type: command, command: npm run lint, timeout: 60 } ] } ] } }这个配置看起来简单但实际用的时候要注意matcher的范围。如果你只希望在前端项目里生效最好配合项目级 settings.json 使用。还有一点lint 脚本本身不能太慢否则每次编辑都要等十几秒体验会很差。我一般选择只跑 syntax check 级别的快速校验全量规范检查放到 CI 里。5.5 场景五Stop 与 SessionEnd 自动备份 通知一个长任务结束人不在电脑前怎么知道它收工了我在 Stop 事件里做一个自动 git 提交SessionEnd 里做桌面通知。Stop 事件里的提交脚本#!/usr/bin/env bash cd $(dirname $0)/../.. || exit 1 git add -A git commit -m chore: auto commit by Claude Code [$(date %Y-%m-%d %H:%M:%S)] || trueSessionEnd 里做通知以 macOS 为例#!/usr/bin/env bash osascript -e display notification Claude Code 会话已结束 with title Claude Code细心的读者会发现自动 commit 这件事有争议万一 Claude 改了不该改的代码自动提交反而会污染历史。我实际项目里的做法是自动提交只放在临时分支上比如claude/auto-YYYYMMDD确认后再合并到主干。自动化不等于放弃可控性Hooks 越强大你越要有意识地给“失控”留缓冲。6. 排错实录我踩过的四个 Hook 坑Hooks 写起来很快但排错可能消耗一整天。我把自己踩过的坑整理成清单按“最可能遇到”排序。6.1 坑一路径不生效~没有展开第一个坑往往出现在“我明明把脚本写在~/.claude/scripts/底下怎么就是找不到”。有时候 settings.json 里写~/scripts/x.sh看似没问题但 hook 执行的环境可能不会展开~导致文件找不到、事件静默失败。排查方法是脚本路径一律写成绝对路径或者用$HOME拼接。比如/Users/yourname/.claude/scripts/x.sh。虽然繁琐但能减少大量玄学问题。脚本内部的子命令引用同样用绝对路径别依赖相对路径。6.2 坑二环境变量缺失命令找不到这个坑常见于从 IDE 或 dock 里启动 Claude Code 的场景。GUI 进程的 PATH 继承自图形环境可能不包含~/.local/bin或/usr/local/bin。于是脚本里写的node、jq、python3可能直接command not found。解决方案是在脚本开头显式导入 PATH#!/usr/bin/env bash export PATH$PATH:$HOME/.local/bin:/usr/local/bin:/opt/homebrew/bin或者更粗暴一点脚本里直接写解释器的绝对路径比如/usr/bin/python3。两种我都用过实测下来“export PATH”更灵活因为脚本内部还可能要调其他工具。6.3 坑三JSON 输出格式损坏导致解析失败Hooks 的 JSON 输出走的是标准输出如果脚本同时打印了普通日志和 JSONClaude Code 在解析时很可能因为混入杂讯而失败。Python 里用print(json.dumps(...))是安全的但如果你用 shell 拼接字符串很容易搞出引号或换行转义的错误。我的建议是输出 JSON 前先用一个临时变量把结构化数据组装好再一次性打印。能不用echo拼 JSON 就不用。如果脚本里还想要日志请写到文件里而不是标准输出。6.4 坑四hook 卡住整个 Agenthook 脚本在 Agent 循环里是同步执行的意味着脚本只要不退出Claude Code 就一直在等。我踩过一次写了个 SessionStart 脚本里面有个网络请求没设超时结果每次会话启动都卡二十多秒。根本解法是给每个 hook 设置合理的timeout。我的经验值纯本地文件操作10 秒以内跑 lint 或编译类命令60 秒任何网络请求必须有内部超时不能裸连排错链路也分享给大家发现问题先手动在终端执行一遍那个命令确认脚本本身没问题再检查输出有没有混入非 JSON 内容最后确认 timeout 是否够用。八成问题在这一步就能定位。7. 进阶思考换模型、MCP 与 Hooks 的分工边界7.1 Hooks 与模型无关换后端也照样跑很多人折腾 Claude Code 的第三方 API 接入把后端模型换成 DeepSeek、Qwen、GLM 之类的。这里有个容易误解的点Hooks 是客户端框架层的事件系统它监听的是 Claude Code 这个客户端的运行节点而不是某个模型。只要你还在用 Claude Code 跑任务不管后端接什么模型Hooks 都会照常触发。这对那些想尝试不同模型的朋友是个好消息你的自动化脚本、安全闸门、日志流水不会因为换个模型就失效。我实测接入第三方模型后SessionStart 的上下文注入和 PostToolUse 的 lint 校验行为完全一致。真正干活的是客户端模型只是推理引擎理解了这个分层你就知道哪些东西要跟着模型走、哪些东西要沉淀在客户端层。7.2 Hooks 与 MCP 的分工有 Hooks 之后很多人会问那还需要 MCPModel Context Protocol吗两者解决的不是一类问题。MCP 是给模型提供“能力和数据”的比如让 Claude Code 查数据库、读监控系统、调用公司内部 API。它扩展的是 Agent 的“手”能伸到多远。Hooks 是给模型循环加“旁路逻辑”的它管的是“工具调用之前做什么检查、会话结束时做什么收尾”。一个是扩展能力一个是定制流程。我用一个表格总结方便对照维度HooksMCP触发方式事件自动触发模型按需调用运行模型本地 shell 脚本独立进程或远程服务典型用途审计、校验、通知、存档实时数据、外部系统、知识库开发成本一个脚本即可需要写一个 server失败影响可能卡住 Agent工具不可用但不阻塞主循环实际项目里两者经常配合MCP 提供了读取数据库的工具PostToolUse 的 hook 可以在模型查完数据库之后自动记录一条审计日志。边界清晰价值两倍。7.3 三条经验之谈最后说点我自己的习惯。第一hooks 脚本务必保持短小我要求自己任何脚本不超过二十行逻辑复杂了就拆成多个事件、多个脚本也不要堆在一处第二每个 hook 运行都往日志目录追加一条带时间戳的记录出问题时翻日志比脑补快得多第三正式环境里尽量不要用 permissionDecision 做全量 allow宁可让权限确认弹窗偶尔烦你一下也不要在高危命令上赌一把。Hooks 这套机制用好了就是给 Claude Code 装上了手脚和眼睛剩下就看你想让它多“操心”了。把那些重复的、机械的、容易出错的环节交给事件脚本你才能真正从“盯着终端”的状态里解放出来。