尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Claude Code Hooks 实战:从安全拦截到自动化工作流

发布时间:2026/9/26 8:41:36

资讯中心
01
ARTICLE

Claude Code Hooks 实战:从安全拦截到自动化工作流

Claude Code Hooks 实战:从安全拦截到自动化工作流
我第一次认真研究 Claude Code Hooks是因为一个差点酿成事故的rm -rf。当时我在自动化重构一批历史代码让 Claude 反复执行清理任务没想到某次它调用的 Bash 命令里包了一个递归删除目标路径只差一个字母就是项目根目录。虽然最后没有出事但我意识到像 Claude Code 这类能力越强的编程代理越需要一套“关键时刻能插一手”的机制。Hooks 就是干这个的。它可以理解成 Claude Code 里的事件钩子在代理调用工具、收到用户输入、完成回复、结束会话等生命周期节点上自动执行你自己写的脚本。这个机制能做的远不止拦截危险命令还可以做审计、通知、自动测试、临时上下文备份。适合谁如果你正在重度使用 Claude Code 做自动化开发、批量改代码、搭 CI 流程或者你只是想让 AI 编程助手的行为更可控这篇都值得看完。我会从事件类型、配置写法、真实案例到排查技巧全部过一遍。1. Hooks 到底是什么先解决“管不住代理”的问题1.1 没有 Hooks 时的尴尬在使用 CLI 版 Claude Code 时模型可以调用 Bash、Read、Edit、Write 等工具。它可以读文件、改代码、执行命令。这个能力非常强但如果你不设限制就会出现几类问题它执行了什么命令你只能事后从 transcript 里翻它可能误删文件或者执行长度惊人的 shell 命令你希望它在跑完测试后立刻告诉你结果但它可能只是默默把输出写在最后你想统计一个会话里模型到底改了多少文件很难自动化。这些不是模型智商问题而是缺少流程控制的“插桩点”。没有 HooksClaude Code 就是一个只能对话和干活的终端代理有了 Hooks它才能真正接入你现成的开发工作流。用生活类比来说Hooks 就像给一个能力很强但容易“太自主”的实习生加上了门禁和日报制度门禁决定它能不能进某个房间日报决定它做完事情后留下多少记录。你不需要每时每刻盯着它只需要在几个关键节点上设置规则。另外要说明的是不管你是用官方模型还是通过兼容接口接其他模型Hooks 都工作在 Claude Code 的本地流程层事件一旦发生就会触发跟模型来源关系不大。1.2 和 Skills、MCP 的分工很多刚接触的小伙伴会把 Hooks 和 Skills、MCP 混在一起。我建议这样区分MCPModel Context Protocol是给 Claude Code 扩展“工具生态”的相当于给代理更多可以调用的外部工具Skills 是给模型补充“领域知识/操作流程”的相当于给代理一本更详细的操作手册Hooks 是控制“工具调用前后、对话生命周期里”这些节点的相当于给工作流加开关、闸门和传感器。举例你想让 Claude Code 能查公司内部系统用 MCP你想让它按团队代码规范改代码把规范写进 Skills你想防止它在执行 Bash 时使用危险命令这就是 Hooks 的活。机制解决什么问题典型动作类比Hooks流程控制与自动化拦截命令、发通知、写审计日志、跑测试门禁、传感器MCP扩展能力边界连接数据库、内部 API、外部服务增加工具、食材Skills补充专业知识提供行业规则、操作手册菜谱、SOP这个定位清晰之后你再看配置就不会迷路。Hooks 不负责“让模型更聪明”它负责“让模型更受控、更好接入现有工程体系”。2. 8 类事件和 matcher 匹配规则先搞懂触发的时机2.1 可以在哪些时机触发Hooks 的设计里最核心的概念是“事件”。Claude Code 在运行过程中会抛出事件你在配置里声明关注哪些事件再把脚本挂上去。我用一张表把常用事件和用途整理出来事件名触发时机典型用途PreToolUse代理调用任何工具之前安全拦截、修改参数、记录调用流水PostToolUse代理调用工具之后校验结果、自动补测试、采集输出NotificationClaude Code 需要向用户发送通知时把重要提醒推到系统通知或 IMUserPromptSubmit用户输入新的 prompt 并提交时审计用户输入、做查询价格估算Stop代理完整生成一段回复、等待用户时触发额外处理比如自动保存上下文SubagentStop子代理Subagent结束运行时汇总子代理产物、清理临时文件PreCompact上下文压缩之前备份当前上下文、导出关键信息SessionStart会话启动时初始化环境、加载任务配置SessionEnd会话结束时清理临时文件、写总结报告、上报指标你可能注意到PreToolUse 是最常用也最“危险”的时机。它发生在工具真正执行前如果想要阻止某个操作这里是唯一能“拦住”的节点。PostToolUse 则适合做“事后验证”比如 Claude 刚改完一个文件hook 立刻跑一遍检查。Stop 和 SessionEnd 适合做收尾类工作SessionStart 适合做环境准备。PreCompact 比较特殊它是在上下文快要被压缩时触发适合把当前讨论结果存到磁盘防止压缩后丢信息。把所有事件放在一起看一个典型会话的触发顺序大概是SessionStart 先跑环境准备用户提交 prompt 后 UserPromptSubmit 被触发接下来模型开始调用工具每次工具调用前 PreToolUse 先跑成功后 PostToolUse 再跑模型一轮回复结束后 Stop 触发如果上下文太长要压缩压缩前会看到 PreCompact会话结束或进程退出时 SessionEnd 收尾。理解这个顺序对设计 hook 很重要比如 SessionStart 里写的临时文件就可以在 SessionEnd 里清理PreToolUse 里临时设置的环境变量不一定能传递到后续异步任务中所以跨 hook 共享状态最好写成文件或持久化到磁盘。2.2 matcher 精确匹配哪个工具在同一类事件下你未必想对所有工具生效。matcher 就是用来做这件事的。举个例子PreToolUse 事件会覆盖 Bash、Read、Edit、Write、WebFetch 等所有工具。如果我只关心 Bash就在 matcher 里写Bash如果我还关心 Edit 和 Write可以用数组matcher: [Edit, Write]。matcher 匹配的字符串要跟官方工具名保持一致大小写很关键。bash和Bash是否等效在部分版本里处理得并不统一所以我的习惯是统一用官方文档里的写法至少在本地锁死一个版本。另外Notification 事件对应的是通知类型比如认证失效、限流、错误等。如果你对某类通知特别敏感也可以用 matcher 单独绑脚本。为了避免配置失效建议先在.claude/settings.json写一个最简单的PreToolUse: {matcher: Bash}测试确认能触发再继续加复杂逻辑。如果某个 hook 始终不触发不要急着怀疑脚本先检查 matcher 是不是匹配错了对象。3. settings.json 配置写法与执行模型3.1 配置文件放哪里Hooks 配置写在 Claude Code 的 settings 文件里通常有三个层级用户级~/.claude/settings.json对所有项目生效项目级.claude/settings.json提交到仓库团队共享本地项目级.claude/settings.local.json不提交仓库个人使用。三个层级会自动合并。我的经验是团队通用的安全拦截和审计逻辑放项目级个人调试用的通知、临时脚本放 local。这样既能让队友共享规则也能避免把自己的实验性脚本推到仓库里污染大家。还有一个经常踩的坑Claude Code 对 settings 文件的命名和路径比较敏感如果文件名不对配置不会报错但 Hooks 就是不触发。改完配置后建议重新启动对话会话让新配置生效。我见过一个项目把配置文件写成了.claude/setting.json少了最后的 s捣鼓了半天才发现是文件名问题。所以排查 Hooks 问题时第一件事永远是确认你编辑的文件确实是 Claude Code 正在读取的那一个。3.2 标准结构长什么样一个真实的 hooks 配置结构如下{ hooks: { PreToolUse: [ { matcher: Bash, hooks: [ { type: command, command: node .claude/hooks/guard-bash.js } ] } ], PostToolUse: [ { matcher: [Edit, Write], hooks: [ { type: command, command: python3 .claude/hooks/run-checks.py } ] } ] } }第一层hooks下的 key 是事件名每个事件对应一个数组数组里是“规则”每条规则有 matcher 和 hooks 两个核心字段。matcher 决定规则适用于哪个工具hooks 是实际要运行的脚本列表每个脚本用type和command描述。command可以直接写 shell 命令也可以指向一个脚本文件。建议所有脚本放在项目内的.claude/hooks目录下并用相对路径或绝对路径引用。因为 CLI 环境里 PATH 不一定像你终端里那么完整直接写node可能找不到踩过好多次。稳妥的做法是在脚本第一行写上完整解释器路径或者 command 里用绝对路径调用。如果你用的是 Node.js最好先which node把路径查出来然后写成/usr/local/bin/node .claude/hooks/xxx.js。3.3 stdin、stdout、退出码、超时当 hook 脚本被触发时Claude Code 会把一个 JSON 事件负载通过标准输入stdin传给脚本。这个 JSON 大约长这样{ session_id: abc123, transcript_path: /path/to/.claude/transcripts/abc123.jsonl, cwd: /path/to/project, hook_event_name: PreToolUse, tool_name: Bash, tool_input: { command: rm -rf ./dist } }具体的字段会根据事件类型有变化比如 PostToolUse 会多出工具输出相关的数据UserPromptSubmit 会有用户提交的 prompt。你在脚本里要做的第一件事就是读取 stdin 并解析 JSON。stdout 对 Claude Code 来说是“反馈给模型的内容”。所以在 PreToolUse 里如果脚本有额外提示想让模型看到可以打印到 stdout但如果只是调试请写到日志文件不要污染 stdout。我的习惯是业务提示走 stdout调试日志走 stderr 或者直接写文件。退出码也非常重要。0 表示脚本正常执行Claude Code 会继续流程非 0 在绝大多数场景下意味着 hook 执行失败Claude Code 会把 stderr 里的内容展示出来PreToolUse 里的工具调用通常也会被中止。于是你想拦截某个命令就让脚本以非 0 退出并输出原因。超时和异步是两个经常被忽略的字段。timeout可以限制脚本最长执行时间async设为 true 时hook 会在后台执行不阻塞代理继续工作。PostToolUse 里做耗时检查、Notification 里发通知都适合开 async。PreToolUse 这种需要决策的拦截一般不要异步否则就失去拦截意义了。需要“决策”的就同步需要“通知/记录”的就异步这是我一直坚持的划分原则。3.4 阻塞与异步的正确姿势如果你把每个 hook 都设成同步很快会发现 Claude Code 的响应变得很慢。比如一个 Notification 脚本要去请求外部接口可能耗时几秒同步模式下模型就得等着这很不划算。反过来如果你把安全拦截脚本设成异步脚本还没跑完工具调用就已经发出了拦截形同虚设。所以在配置之前先问自己一个简单问题这个 hook 是否需要影响 Claude Code 的下一步行为如果需要就同步并且设置合理的 timeout如果只是记录、通知、事后上报就异步。异步 hook 有一个隐蔽的好处即使脚本本身出 bug也不会阻塞用户的主流程非常适合放在生产环境里做观测类功能。我自己的项目里危险命令拦截、用户输入审计是同步系统通知、指标上报是异步。4. 四个可直接抄的实战 Hook4.1 在 PreToolUse 里拦截高危 Bash 命令目标禁止代理执行包含危险模式的命令。先写一个 Node.js 脚本#!/usr/bin/env node const fs require(fs); const input fs.readFileSync(0, utf8); const event JSON.parse(input); if (event.hook_event_name PreToolUse event.tool_name Bash) { const cmd event.tool_input event.tool_input.command || ; const dangerous [ /rm\s-rf\s\/(?!tmp)/, /git\spush\s--force/, /curl\s.*\|\s*(ba)?sh/, /\s*\/dev\/sd/ ]; for (const pattern of dangerous) { if (pattern.test(cmd)) { console.error([hook] blocked dangerous command: ${cmd}); process.exit(1); } } } process.exit(0);然后在设置里挂上{ hooks: { PreToolUse: [ { matcher: Bash, hooks: [ { type: command, command: node .claude/hooks/guard-bash.js, timeout: 10 } ] } ] } }这个方案的核心逻辑是脚本解析 stdin 里的 JSON拿到tool_input.command然后用一组正则判断是否命中危险模式。一旦命中就通过 stderr 输出原因并以非 0 退出让 Claude Code 中断这次工具调用。这里要特别提醒正则的颗粒度。写得太死比如只拦rm -rf /那漏掉rm -fr /、rm -rf --no-preserve-root /一样完蛋写得太宽比如看到rm -rf就拦Claude 连清理node_modules这种正常操作都会被卡住交互体验非常差。我在实际项目里维护了一个独立的危险命令模式文件而不是把规则写死在脚本里这样更新规则不用改代码。脚本每次启动时重新加载这个文件也方便团队评审。4.2 在 Notification 里推送系统通知Claude Code 在需要用户注意时会触发 Notification 事件比如认证失效、长时间任务结束。用 hook 把它变成系统通知比一直盯着终端舒服。脚本逻辑如下#!/usr/bin/env bash event$(cat) echo $event /tmp/claude-notifications.log if command -v terminal-notifier /dev/null 21; then echo $event | jq -r .message // Claude Code notification | terminal-notifier -title Claude Code elif command -v notify-send /dev/null 21; then notify-send Claude Code $(echo $event | jq -r .message // Notification) fi配置时注意把async设成 true因为通知不应该阻塞主流程{ hooks: { Notification: [ { matcher: *, hooks: [ { type: command, command: bash .claude/hooks/notify.sh, async: true } ] } ] } }这里有个细节matcher 用*表示匹配全部通知类型实际使用前建议先在当前版本里验证一下通配符是否生效如果解析有问题就改成你真正关心的具体通知类型。通知类 hook 的特点是“丢了也就丢了”所以异步完全没问题。我把所有通知先写进一个日志文件这也是排查问题的退路就算系统通知没弹出来也能查日志确认事件确实发生过。4.3 在 UserPromptSubmit 里写审计日志如果你需要知道每个会话里用户到底让 Claude 干什么UserPromptSubmit 是最合适的切入点。一个 Python 脚本示例#!/usr/bin/env python3 import sys, json, time, os event json.load(sys.stdin) log_line { ts: time.time(), cwd: event.get(cwd, ), session_id: event.get(session_id, ), prompt: event.get(prompt, ), hook_event_name: event.get(hook_event_name, ), } log_path os.path.join(event.get(cwd, os.getcwd()), .claude, logs, prompt-audit.jsonl) os.makedirs(os.path.dirname(log_path), exist_okTrue) with open(log_path, a, encodingutf-8) as f: f.write(json.dumps(log_line, ensure_asciiFalse) \n)这个脚本的逻辑很简单读取 stdin 里的 JSON把时间戳、工作目录、会话 ID、用户输入写入一个 JSONL 文件。JSONL 的好处是每一行都是独立 JSON后面用jq或者 Python 做统计都非常方便。这个 hook 我建议同步执行因为审计日志必须保证在用户输入被真正处理之前落盘。你可以为它加一个很短的 timeout比如 5 秒这样如果磁盘出问题也不会无限阻塞会话。实际运行中要特别注意编码问题用户输入可能是中文、emoji 或者特殊转义符写文件时记得用ensure_asciiFalse否则后续做文本分析时会很痛苦。我在团队里用这个日志做过一个简单报表按天统计每个会话里用户最常提的诉求对评估自动化场景很有帮助。4.4 在 PostToolUse 后自动跑与变更相关的测试思路监听 Edit/Write当改动的是源码目录或测试目录时异步跑一次轻量测试。脚本用 Python 提取tool_input.file_path然后根据扩展名和目录决定是否要触发测试。简单版本如下#!/usr/bin/env python3 import sys, json, os, subprocess event json.load(sys.stdin) if event.get(hook_event_name) ! PostToolUse: sys.exit(0) file_path event.get(tool_input, {}).get(file_path, ) if not file_path: sys.exit(0) # 只关心 Python 源码和测试文件 if not (file_path.endswith(.py)): sys.exit(0) # 目录过滤避免对临时文件、日志目录做检查 if /.claude/ in file_path or /.venv/ in file_path: sys.exit(0) cwd event.get(cwd, os.getcwd()) result subprocess.run( [pytest, -q, --maxfail1], cwdcwd, capture_outputTrue, textTrue, timeout120, ) with open(os.path.join(cwd, .claude, logs, post-tool-test.log), a, encodingutf-8) as f: f.write(f{file_path}\nreturncode{result.returncode}\n)然后配置{ hooks: { PostToolUse: [ { matcher: [Edit, Write], hooks: [ { type: command, command: python3 .claude/hooks/run-checks.py, async: true, timeout: 130 } ] } ] } }这种 hook 最大的问题是容易触发“风暴”Claude 连续编辑多个文件时PostToolUse 会反复触发每次跑一遍 pytest 会让 CPU 瞬间拉满。我在生产脚本里加了一个基于时间戳的节流器如果距离上次运行不足 30 秒直接退出。另外如果 hook 脚本本身又去调用 Claude Code CLI就可能形成递归触发所以我在脚本入口加了一个环境变量标记子进程里如果看到这个标记就直接退出。用异步既能拿到测试结果又不会让 Claude 每次编辑都被迫等待。5. 调试技巧、常见问题与避坑清单5.1 确认 hook 真的触发了很多配置不生效是路径或文件名的问题。第一条经验hook 脚本里第一行就把入参原样写到日志文件cat /tmp/hook-debug.log然后手动触发一次事件看/tmp/hook-debug.log里有没有内容。如果有说明触发链路是通的。如果没有优先检查 settings 文件的位置、配置文件名、事件名拼写以及是否重启了会话。第二条经验不要把调试信息直接 print 到 stdout除非你明确知道它是给模型看的。需要记录就写 stderr 或写文件。因为 stdout 在部分事件里会被 Claude Code 当作给模型的补充信息掺杂调试内容容易干扰模型判断。第三条经验把 hook 拆成“最小可跑脚本”先让脚本单独执行echo {} | node script.js确认脚本本身没问题再挂到 Claude Code 上。这样能快速区分是脚本 bug 还是配置问题。5.2 高频问题速查表现象可能原因解决思路hook 完全没触发settings 文件路径不对、事件名写错用 debug 脚本验证并重启会话提示 command not foundhook 环境 PATH 精简脚本或 command 中改绝对路径JSON 解析失败脚本没读 stdin或读到多余输出只读 stdin别让脚本向 stdout 打印解释性文字hook 把会话拖住同步执行的时间太长加 timeout或改用 async模型总被奇怪的输出干扰stdout 被 Claude 拿到调试日志写文件不要写 stdout同一操作重复触发脚本里又调用了 Claude Code加环境变量或标记位防止递归配置改了不生效缓存/会话未重启重启会话确认修改的是合并后生效的那个 settingsSessionStart 里相对路径失效cwd 不在项目根目录用绝对路径或从事件 JSON 里的 cwd 字段拼路径这些都是我实际遇到过的。Hook 排查最大的难点不在于脚本逻辑而在于“触发链路不可见”。所以任何项目接手时我都建议先搭一个日志钩子把所有事件打出来观察一段时间再决定加什么规则。比如我把日志钩子挂在 SessionStart 上每次启动都会打印当时的 cwd 和配置路径这样连环境变化都能看到。5.3 关于 Hooks 的三点心得第一别贪多。Hooks 数量越多agent 的响应越慢出问题的地方也越多。我一般只保留三类安全拦截、审计日志、关键通知。像“每次读文件都记录”“每次编辑都备份”这种高频率 hook开销非常大容易把一次本来很简单的任务拖成龟速。第二保持脚本幂等。同一个事件可以触发很多次SessionStart 可能开多个子会话PreToolUse 在重试时可能再次触发。脚本设计成无论执行多少次结果都一样才不会给自己挖坑。第三把失败当成功能。Hook 脚本的退出码和 stderr 是 Claude Code 与模型、用户的沟通渠道。安全类 hook 拦截时不要只输出Error要尽量给出可理解的拒绝原因比如“检测到危险命令已阻止”否则模型只会看到一串错误不知道下一步该怎么调整。最后分享一个我最近在用的扩展方向。我在本地搭了一个非常轻的日志服务把 SessionEnd 和 UserPromptSubmit 的 JSON 负载直接打到这个服务的 HTTP 接口然后脚本按角色和耗时做了简单的报表。这样一来每周能看出 Claude Code 到底把时间花在了哪些任务上。Hooks 真正的价值不是限制模型而是让模型和你现有的工程体系、监控体系、通知体系彻底打通。你先从一个小小的拦截脚本开始跑通一次事件触发后面就会发现能玩的花样越来越多。我个人踩过最深的坑是配置路径写错导致 hook 完全静默所以请务必先让日志跑起来再谈其他。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。