Claude Code Hooks完全指南:手把手教你拦截危险命令和自动化任务
1. Hooks 到底是什么:先搞清楚这个功能解决什么问题
用过 Claude Code 的人应该都有这种体验:它确实能帮你写代码、改文件、跑命令,但有时候它“太自由了”——想改哪个文件就改哪个文件,想执行什么命令就执行什么命令。比如你在一个团队项目里跑 Claude Code,它突然给你重构了整个目录结构;或者你让它跑一个测试命令,它顺手把日志文件全删了。这种失控感,尤其是在生产环境或者多人协作的项目里,基本是没法接受的。
Hooks 就是 Claude Code 专门用来解决这类问题的一套机制。说人话,它就是在 Claude Code 的整个工作流程里,给你预设了几个“挂钩子”的位置。你可以把自己的脚本挂在这些位置上,当 Claude Code 执行到某个阶段时,自动触发你的脚本,由你的脚本来做检查、拦截、记录、改造等操作。
举个例子。Claude Code 在执行一个工具调用之前,会先触发 PreToolUse 这个钩子。你在钩子里写一段脚本,检查 Claude 到底要执行什么命令。如果发现它想执行 rm -rf / 这种危险操作,你的脚本直接返回一个“拒绝”的退出码,Claude Code 就会放弃这次操作,并且把这个拦截结果反馈给 Claude 模型,让它换个思路。整个过程完全自动,不需要你坐在旁边盯屏。
这功能解决的是几个实实在在的痛点。第一个是安全管控,防止 AI 执行危险命令、改动不该改的文件,尤其是误操作删除数据这种事故;第二个是自动化规范,比如每次工具调用结束后自动格式化代码、自动补丁、自动生成变更日志,不用你手动再去跑一遍;第三个是流程接入,把 Claude Code 的生成结果接入到你已有的 CI/CD 流程、消息通知、监控系统里,真正让 AI 编程这件事融入到工程体系里,而不只是“开个终端陪聊写代码”。
对于个人开发者、团队管理者、DevOps 工程师,甚至只是对 Claude Code 感兴趣想深入折腾的人来说,Hooks 都是一个值得花时间搞明白的功能。这篇内容我尽量把配置方式、事件类型、实际脚本、踩坑记录都写清楚,反正我自己折腾的时候踩了不少坑,能帮你省点时间就是好的。
2. Hooks 的完整事件模型:搞清楚什么时候触发,才能写好脚本
Hooks 不是只有一个“万能钩子”,而是分成了好几类事件。每一类事件对应 Claude Code 生命周期里的一个特定环节。先把这些事件彻底搞懂,后面写配置和脚本就不会懵。
2.1 六大 Hook 事件逐个拆解
官方目前支持的 Hook 事件主要有六种:PreToolUse、PostToolUse、Notification、UserPromptSubmit、Stop、SubagentStop。我按它们在一次典型交互中出现的先后顺序来说。
PreToolUse 是在 Claude 决定要调用某个工具之前触发的。这里说的“工具”包括执行 Bash 命令、读写文件、编辑文本、发起网络请求等等。这个钩子是最常用、也最实用的一个,因为它是“拦截点”,你可以在这里阻止 Claude 做某些事情。比如你不想让 Claude 执行任何 git push 操作,就在这里拦截。
PostToolUse 是在某个工具执行完毕之后触发的。Claude 已经跑完了命令、已经改完了文件,这时候钩子触发。这个钩子适合做“善后事项”,比如代码格式化、自动补丁、收集执行结果发给后端、生成操作审计日志。
Notification 是在 Claude 主动发起通知时触发的。Claude 可以在对话过程中通过特定机制向用户发通知,比如任务完成了、需要用户确认某些事情。这个钩子可以把通知转发到你的 IM 工具、邮箱或者其他地方,适合做长时间任务的提醒。
UserPromptSubmit 是在你向 Claude 提交新的提示词时触发的。注意这个和 PreToolUse 是有本质区别的,前者发生在“用户输入新指令”时,后者发生在“Claude 调用工具”时。这个钩子适合做输入侧的处理,比如敏感信息过滤、prompt 模板注入、记录用户的输入日志。
Stop 是在 Claude 完成一次完整回复、准备停止的时候触发的。如果你用 Claude Code 跑批处理任务,这个钩子可以帮你判断任务是否真的完成了。顺带一提,Stop 的触发优先级要高于 PreToolUse,这个顺序在编写依赖钩子执行结果的脚本时要注意。
SubagentStop 是在 Claude 的子代理(Subagent)完成工作时触发的。子代理是 Claude Code 里比较新的一种能力——让一个专门的 Claude 实例去处理某个子任务,主任务等它返回结果。如果你需要精确感知子任务的状态变化,就用这个钩子。
2.2 触发时机与典型场景选择
说了这么多,到底什么时候用哪个?我给你一个比较实用的对照参考:
| 事件类型 | 触发时机 | 典型场景 |
|---|---|---|
| PreToolUse | Claude 调用工具前 | 命令白名单/黑名单校验、文件路径保护、敏感操作拦截 |
| PostToolUse | 工具执行完毕后 | 自动格式化、日志收集、审计记录、自动打补丁 |
| Notification | Claude 发送通知时 | 飞书/钉钉/邮件消息推送、任务进度提醒 |
| UserPromptSubmit | 用户提交新消息时 | 敏感词过滤、prompt 日志记录、上下文注入 |
| Stop | 一次回复结束时 | 批处理任务收尾、执行结果汇总 |
| SubagentStop | 子代理任务结束时 | 子任务状态管理、并行任务编排 |
实际项目中,最常用的是 PreToolUse 和 PostToolUse 两个。我见过的大多数 Hooks 场景,基本都围绕“执行前拦截危险操作”和“执行后自动规范化”展开。Notification 适合接消息推送,Stop 适合做批处理流程。UserPromptSubmit 和 SubagentStop 相对小众一些,但有它的价值。
2.3 matcher 匹配规则:怎么精确控制“哪些工具触发钩子”
每种 Hook 事件在配置时还能再加一个 matcher 参数,用来匹配“具体哪个工具”触发这个钩子。PreToolUse 的 matcher 可以匹配 Bash 、 Read 、 Edit 、 Write 等工具名称,可以用通配符 * 匹配所有工具。PostToolUse 同理。
matcher 支持通配符,比如 Bash* 就能覆盖所有以 Bash 开头的工具。多个匹配条件可以用逗号分隔,比如 Bash,Edit 就是同时匹配执行命令和编辑文件两种操作。这个设计非常实用——你可以为不同工具分别配置不同的钩子逻辑。比如针对 Bash 命令做严格的命令白名单,而对 Read 操作不做任何拦截。
3. 配置文件的完整解析:settings.json 里到底能写什么
Hooks 的配置放在 Claude Code 的配置文件里面,这个文件就是 settings.json 。理解这个文件的层级关系是所有配置工作的基础。
3.1 全局配置与项目配置的层级关系
Claude Code 支持两个层级的配置:项目级和用户级。
项目级配置放在当前项目的 .claude/settings.json 里(有些版本也支持 ~/.claude/settings.json 作为用户级)。项目级配置随仓库走,如果你把 .claude 目录提交到 Git,团队其他人拉下来就自动生效了。这个特性非常适合团队统一管理 Hooks。但要注意,如果是把 .claude/settings.json 提交到仓库,用户级的 settings 是存在 ~/.claude/settings.json 的,这个不会进仓库。
两个层级同时存在时,配置会做合并。所以你可以这样规划:项目级的 settings 放 Hooks 相关的团队规范配置,用户级的 settings 放个人的偏好配置(比如权限模式、编辑器偏好)。合入时具体某些字段是覆盖还是追加,跟具体的键有关系,Hooks 这一块的实际表现是:用户级配置和项目级配置的 hooks 会合并生效,同名同 matcher 的项会都执行。这个行为建议不要依赖,因为版本之间的行为不完全一致,最稳妥的方式是团队规范一律进项目级配置。
3.2 一个完整的 hooks 配置示例逐字段解读
先看一个完整的配置长什么样:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/pre_tool.py"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/format.sh"
}
]
}
],
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/notify.py"
}
]
}
]
}
}逐个解释一下这个配置的含义。最外层是 hooks 对象,下面每个 key 对应一种事件类型。每种事件类型是一个数组,数组里的每个对象都可以有两个字段: matcher 和 hooks 。 matcher 指定匹配哪些工具名,留空或不写就匹配全部。 hooks 是实际要执行的脚本列表,每个脚本由 type 和 command 定义,目前 type 固定为 command , command 就是要执行的 shell 命令。
注意这里的层级关系——事件类型下先通过一个数组分组,每个分组里可以有多个 hooks,每个 hooks 里也可以有多条命令。同一个 matcher 下要执行多件事,直接在 hooks 数组里加对象就行,它们会按顺序执行。
3.3 退出码的约定:0、2 和 1 分别代表什么
Hooks 脚本的退出码是整个机制里最关键的约定,直接影响 Claude Code 能不能继续往下走。
退出码为 0 时,表示钩子执行成功、放行,Claude Code 继续执行原本要做的操作。退出码为 2 时,表示钩子执行成功但要求阻止——Claude Code 会停止当前操作,把钩子的输出信息返回给 Claude 模型,让 Claude 根据这些信息重新调整方案。退出码非 0 非 2(比如 1),表示钩子自身执行出错,这种情况下 Claude Code 会报错并终止当前动作,同时把错误信息显示出来。
这个设计非常巧妙。0 和 2 的区别,相当于“测试通过”和“测试被跳过但主动叫停”的区别。实际编写脚本时,善用退出码 2 来实现“软拦截”——比如 Claude 想执行一个你没白名单的测试命令,你希望它能换个命令,那就在钩子里输出提示后返回 2,Claude 看到返回结果后会明白自己不该执行这个命令,从而主动调整。
有一个容易踩的坑是:脚本内部的错误处理和退出码混淆。比如你的 python 脚本里检测到了一个危险命令,但你用了 exit(0) ,什么拦截效果都没有;反之,你想拦截,但脚本瞬间崩了返回 1,Claude Code 也会直接中断任务,不给你反馈的机会。所以脚本里要把“正常拦截返回 2”和“脚本异常返回 1”严格区分开。
4. 环境变量与参数传递:脚本怎么拿到上下文信息
要让 Hooks 脚本真正干实事,光靠固定命令是不够的。Claude Code 会给每个钩子注入一批环境变量,脚本通过读环境变量来感知当前的状态,比如当前调用的是什么工具、参数是什么、项目根目录在哪,等等。
4.1 重点环境变量清单
根据实际使用经验,有几个环境变量一定要掌握:
CLAUDE_PROJECT_DIR 表示当前项目的根目录。如果你的脚本要读写项目里的文件,建议一律基于这个变量来拼路径,不要用相对路径。因为 Hooks 脚本的实际工作目录未必是项目根目录,这在多目录切换、通过命令行标志指定项目目录时会出问题。
CLAUDE_TOOL_NAME 表示触发钩子的工具名称,比如 Bash 、 Read 、 Edit 。这个变量在 PreToolUse 和 PostToolUse 里都有。你可以根据工具名称在同一个脚本里做不同逻辑。
CLAUDE_TOOL_INPUT 表示工具的输入参数,是一个 JSON 字符串。比如工具是 Bash ,这个字段里的 JSON 就包含 command 和 timeout 等字段;工具是 Edit ,就包含 file_path 、 old_string 、 new_string 等字段。这个变量是钩子脚本做逻辑判断的核心数据来源——你想判断 Claude 是否要执行危险命令,就要解析这个 JSON。
CLAUDE_TOOL_OUTPUT 只在 PostToolUse 阶段存在,表示工具的执行输出结果。比如 Bash 命令执行后的 stdout、stderr。注意这个值可能很大,如果你的脚本要处理它,最好做截断或者摘要处理,免得环境变量撑爆内存。
其他还有一些辅助变量,比如 CLAUDE_HOOK_EVENT 表示当前触发的事件类型, CLAUDE_HOOK_MATCHER 表示匹配的规则, CLAUDE_PLUGIN_ROOT 是在插件场景下使用的。不同版本之间可能有差异,写脚本前先用临时钩子把环境变量全打出来看一遍最靠谱。
4.2 用临时脚本快速摸底:打印所有环境变量
这里分享一个我当初摸底用的方法。先写一个简单的脚本,把环境变量都 dump 到日志文件里:
#!/bin/bash env | sort > /tmp/claude_hook_env_$(date +%s).log echo "Hooks 环境变量已打印" exit 0
把它配置到一个 matcher 为 Read 的 PreToolUse 钩子上。随便让 Claude 读一个文件,然后去看 /tmp 目录下的日志文件。这样就能直观看到当前版本的 Claude Code 在触发钩子时到底注入了哪些环境变量。不同版本、不同用法下变量名可能略有差异,这套摸底方法适用于任何版本。注意每次触发文件名都带时间戳,别搞混了。
5. 实战一:PreToolUse 拦截危险 Bash 命令
理论知识铺垫完了,直接进入实操。这个环节我会带你把三种最常见的场景完整实现一遍,从需求分析到脚本编写再到配置上线,每个细节都拆开讲。
5.1 需求分析与设计思路
首先第一个场景。假设你是一个团队的维护者,团队成员都在用 Claude Code 写代码。你希望设置一条底线:Claude 在执行 Bash 命令时,不能运行 rm -rf (尤其是针对整个目录或根目录的)、不能执行 git push --force 、不能运行任何包含 sudo 的命令。要是在人工审核的情况下管理员主动跑这些命令,那是另一回事;但对于 Claude 来说,这些操作都太危险,最好的策略是一律禁止,让 Claude 改用更稳妥的方案。
设计思路是:在 PreToolUse 阶段匹配所有 Bash 工具调用,解析 CLAUDE_TOOL_INPUT 里的 command 字段,检测是否命中黑名单。命中就输出警告信息并返回退出码 2,把这个操作挡回去;没命中就安静地返回 0,放行。
这里有个细节值得展开说。为什么用退出码 2 而不是直接终止进程?因为退出码 2 会把钩子的 stdout 内容作为反馈送给 Claude 模型。也就是说,Claude 会看到你的警告信息,明白自己踩了红线,然后它会主动调整策略,换一种不触发警告的方式完成任务。这比直接中断友好得多,也更符合 AI 编程助手的工作方式。
5.2 黑名单校验脚本实现与逐段讲解
下面直接写脚本。我用 Python 实现,因为 JSON 解析和字符串匹配更顺手。
#!/usr/bin/env python3
import json
import os
import re
def main():
tool_input_raw = os.environ.get("CLAUDE_TOOL_INPUT", "{}")
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", "")
try:
tool_input = json.loads(tool_input_raw)
except json.JSONDecodeError:
print("错误:无法解析工具输入,已放行但请检查 Claude Code 环境。")
return 0
command = tool_input.get("command", "").strip()
if not command:
return 0
# 黑名单规则:每一条是一个正则表达式
blocked_patterns = [
r"\brm\s+-rf?\s+[~/]", # rm -rf 指向home或根目录
r"\bgit\s+push\s+.*--force", # git push --force
r"\bsudo\b", # 任何 sudo
r"\bdd\s+if=.*\s+of=/dev/", # dd 写入裸设备
r":\(\)\s*\{.*\};", # fork 炸弹
]
for pattern in blocked_patterns:
if re.search(pattern, command):
print(f"已阻止危险命令:{command}")
print(f"命中规则:{pattern}")
print("请更换更安全的做法,或分步执行相关操作。")
return 2
# 额外检查:如果命令包含管道连接,且任一段命中黑名单,也要拦截
segments = command.split("|")
for segment in segments:
for pattern in blocked_patterns:
if re.search(pattern, segment.strip()):
print(f"已阻止包含危险子命令的命令:{command}")
return 2
return 0
if __name__ == "__main__":
sys.exit(main())注意开头有一个 sys.exit(main()) ,但上面只 import os, re, json ,没有 import sys 。这个细节在真实环境中很致命——脚本会直接 NameError,返回非 0 退出码,导致所有 Bash 调用都被中止。我在实际配置中第一次跑就踩了这个坑,所以提醒一下:Python 脚本中 import sys 别漏。
解决方法是把第一行改成:
import json import os import re import sys
然后配置段如下:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/block_dangerous.py"
}
]
}
]
}
}为什么用 python3 而不是 python ?因为很多新系统里 python 命令指向的是 Python 2 或者干脆不存在,用 python3 最稳。如果你的机器上 Python 3 不在 PATH 里,就写绝对路径。还有一点,命令里建议使用相对路径时要确定 Claude Code 的当前工作目录,最保险的方案是脚本内基于 CLAUDE_PROJECT_DIR 重定位,比如:
command: "cd \"$CLAUDE_PROJECT_DIR\" && python3 .claude/hooks/block_dangerous.py"
5.3 拦截效果实测记录
配置完成后,我在一个临时目录里实测了一轮。我先让 Claude “删除一下当前目录下的临时文件”,它准备执行 rm -rf ./tmp 。钩子触发,脚本检测到 rm -rf 指向当前目录下的路径,虽然没命中 [~/] 规则(因为路径是 ./tmp ),但命令是 rm -rf ./tmp ,这个其实是合理的操作,我想测试的是拦截效果,就用了测试命令 rm -rf ~/fake_dir 。脚本返回 2,Claude Code 显示“操作已被 Hooks 阻止”,Claude 随即回复:“好的,我改用安全的方式:清空目录内的文件而不是删除目录本身。”
这里注意,黑名单规则里我写了 rm -rf [~/]* 的匹配模式,意味着只拦截指向 home 或根目录的删除操作,对于删除项目内目录的 rm -rf ./xxx 是放行的。这个策略也是深思熟虑过的——完全禁止 rm -rf 会严重影响 Claude 处理项目的效率,真正危险的是删除 home 或根目录级别的大范围操作。你要根据自己的风险偏好调整这条规则。
6. 实战二:PostToolUse 自动格式化与代码规范检查
第二个常见场景是代码规范化。很多团队用 Claude Code 生成代码时总会遇到格式不统一的问题。Claude 生成的代码大体上逻辑没问题,但缩进、引号、行尾空格这些细节,在不同生成批次之间经常不一致。每次人工跑一遍格式化也很烦。
6.1 为什么选择在 PostToolUse 阶段做格式化
PostToolUse 阶段做格式化的核心优势是精准。Claude 每次编辑完一个文件,这个钩子就立刻对这个文件做一次格式化。相比在 Stop 阶段一次性格式化所有文件,这种方法的好处是:只处理被改过的文件,耗时极短;如果格式化结果引发新的问题(比如 parse error),钩子执行过程中就能发现。
还有一个细节:PostToolUse 修改了文件内容后,Claude Code 可能不会重新读取新内容。我实测过,PostToolUse 钩子里改了文件,AI 后续的对话中关于这个文件的内容,有时候是钩子执行之前的内容。这是个比较微妙的问题,目前我的处理方式是把格式化这种“确定性的修改”放心交给钩子做,因为格式化只会让代码更标准,不会改变逻辑,所以即使 AI 没重新读文件,影响也不大。但如果是“AI 生成内容 + 钩子改逻辑”的组合,就有风险了。
6.2 格式化脚本与配置
我这里以 Python 项目为例写了一版可用的格式化脚本:
#!/usr/bin/env python3
import json
import os
import subprocess
import sys
def main():
tool_input_raw = os.environ.get("CLAUDE_TOOL_INPUT", "{}")
tool_name = os.environ.get("CLAUDE_TOOL_NAME", "")
try:
tool_input = json.loads(tool_input_raw)
except json.JSONDecodeError:
return 0
file_path = tool_input.get("file_path", "") if tool_name == "Edit" else tool_input.get("path", "")
if not file_path:
return 0
if not file_path.endswith(".py"):
return 0
# 对 Python 文件执行 black + isort
commands = [
["isort", file_path],
["black", file_path],
]
for cmd in commands:
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=30
)
# 如果格式化工具返回非0,输出警告但不阻塞
if result.returncode != 0:
print(f"格式化警告({os.path.basename(cmd[0])}): {result.stderr.strip()}")
except Exception as e:
print(f"格式化失败: {e}")
return 0
if __name__ == "__main__":
sys.exit(main())这里的关键点是“格式化失败不能阻塞”。因为 PostToolUse 是在工具执行完后触发的,如果你在这里返回 2,反而会把 AI 已完成的操作“作废”,让 Claude 认为自己的操作被拒绝了。但实际上文件已经改过了,这就产生了状态不一致。所以 PostToolUse 的钩子,除非是审计类逻辑,否则一般不建议返回 2。有问题输出到 stdout 提醒用户就够了。
对应配置:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/format_file.py"
}
]
}
]
}
}6.3 带 lint 检查的组合式钩子写法
如果你还想顺带跑一下 ESLint 或 Ruff 之类的 lint 工具,可以在这个脚本后面加上 lint 步骤。运行时注意 timeout 设置,项目大了 lint 可能跑几秒甚至更久,不要让钩子拖垮主线程。我的建议是:格式化是确定性的、必须同步执行;lint 是参考性的,可以考虑放进后台任务,或者只输出摘要结果,不要全量输出到控制台,否则刷屏刷得没法看。
实际项目里我给这个脚本加了一个小改动:只对 file_path 属于当前项目目录下的文件执行格式化,避免 PostToolUse 处理了其他路径的文件(比如 Claude Read 了 /tmp/xxx.py ,然后你给这个临时文件也格式化了,没必要)。
7. 实战三:Notification 与 Stop 配合实现任务通知
第三个场景可能很多人没想过,但实际用起来是真香:长时间任务跑完自动发通知。
7.1 需求场景:语法检查完成后把结果推到 IM
你让 Claude Code 跑一个很耗时的任务,比如全量测试、批量数据迁移、大型重构。执行时间是十几分钟甚至更久,你不想一直盯着终端。这时候如果能自动把结果发给手机上的聊天工具,人就可以放心去做别的事,任务一完成手机就收到通知。
这就是 Notification 钩子的用武之地。Notification 是在 Claude 主动发通知的时候触发。如果你希望通过 Stop 钩子在每次回复结束时都发一个“任务已完成”的通知,也可以用 Stop 钩子。
7.2 飞书机器人通知脚本实例
我这边用的比较多的是飞书机器人,因为自定义机器人配置简单,webhook 直接发 POST 请求就行。下面这个脚本就是一个可直接改来用的实现:
#!/usr/bin/env python3
import json
import os
import subprocess
import sys
import urllib.request
WEBHOOK_URL = os.environ.get("FEISHU_WEBHOOK_URL", "")
def send_feishu_message(title: str, content: str):
if not WEBHOOK_URL:
return
data = {
"msg_type": "interactive",
"card": {
"header": {
"title": {"tag": "plain_text", "content": title},
"template": "blue"
},
"elements": [
{"tag": "div", "text": {"tag": "lark_md", "content": content}}
]
}
}
req = urllib.request.Request(
WEBHOOK_URL,
data=json.dumps(data).encode("utf-8"),
headers={"Content-Type": "application/json"}
)
try:
with urllib.request.urlopen(req, timeout=5) as resp:
pass
except Exception as e:
print(f"发送飞书通知失败: {e}")
def main():
event = os.environ.get("CLAUDE_HOOK_EVENT", "unknown")
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", "")
if event == "Notification":
content = os.environ.get("CLAUDE_NOTIFICATION_CONTENT", "Claude 发送了一条通知")
send_feishu_message("Claude 通知", content.replace("\n", "\n\n"))
elif event == "Stop":
# 获取最后一次工具调用的输出摘要,如果有的话
tool_output = os.environ.get("CLAUDE_TOOL_OUTPUT", "")
if len(tool_output) > 200:
tool_output = tool_output[:200] + "..."
send_feishu_message(
"Claude 任务已结束",
f"项目:{project_dir}\n最后输出:{tool_output or '无'}"
)
return 0
if __name__ == "__main__":
sys.exit(main())注意这里用了一个环境变量 CLAUDE_NOTIFICATION_CONTENT ,在 Notification 事件里保存的是通知内容。这个变量名在不同版本里可能不一样,还是那句话,先用 dump 脚本摸底。
另外,网络请求必须设置超时。如果 webhook 地址不通,不能让它卡住 Claude Code 的主流程。5 秒超时已经是底线了,再多就会明显拖慢交互体验。
7.3 异步发送不阻塞主流程的优化
上面的实现是同步阻塞的。通知请求发出去要等 5 秒超时才返回,这期间 Claude Code 会卡住,影响体验。我实际测试过,正常网络环境下飞书请求一般 0.5 秒内能返回,但万一网络抖动,5 秒的阻塞就很明显。
优化方案是:把通知发送放到后台子进程。
nohup python3 .claude/hooks/notify.py > /tmp/claude_notify.log 2>&1 &
这样主线程立即返回,通知在后台慢慢发,完全不影响 Claude Code 的交互。我建议所有通知类的钩子都用这种方式,不值得为了一个消息通知影响主流程。
8. 配置的多个层级:权限与作用域的正确理解
前面顺带提过 settings.json 有两种层级,但实际使用中还有更细的作用域差异。这里专门展开讲,因为很多人就是在这里被绕晕的。
8.1 项目级、用户级、局部配置三者的取舍
Claude Code 配置比较常见的分层是: --settings 参数指定的配置文件、项目级 .claude/settings.json 、用户级 ~/.claude/settings.json 。另外,还有一些代码仓库中可能带有的 .claude/settings.local.json ,通常用于存放个人本地配置。
我实际推荐的用法是:
- 团队规范类 Hooks(命令拦截、自动格式化)放项目级
.claude/settings.json,提交到 Git,随仓库分发。 - 个人偏好类配置(比如用户自己的 webhook 地址、个人通知配置)放用户级
~/.claude/settings.json,不强制进仓库。 - 敏感信息(webhook URL、token)不要硬编码在项目配置文件里,用环境变量注入。
这里有一个安全提醒:webhook 地址虽然不算高敏感,但如果你把含有 webhook 的 settings 文件提交到公开仓库,别人就可以给你的机器人发消息。更好的方案是脚本里从本地环境变量读取,比如 .bashrc 或 .zshrc 里 export FEISHU_WEBHOOK_URL=xxx 。
8.2 权限模式对 Hooks 行为的影响
Claude Code 的权限模式会影响 Hooks 的执行方式。在默认的自动接受模式下,Claude 可以自主执行工具调用,Hooks 是主要的安全防线。但在 plan 模式(规划模式)下,Claude 不会执行实际的操作,大多数工具调用会被限制,Hooks 自然也不会被触发。你必须切换到正常的执行模式,Hooks 才会生效。
这个细节在调试时很容易迷惑人——明明配置了 Hooks,但没触发。先看看自己是不是处于 plan 模式。
9. 调试排错:我踩过的坑和解决方法
9.1 开启详细日志,快速定位问题
Claude Code 支持通过 --debug 标志开启详细日志输出。当你怀疑 Hooks 没有正确触发或者脚本异常时,先用这个模式跑一遍:
claude --debug
打开调试模式后,终端会输出更详细的内部运行信息,包括每个 Hook 事件何时触发、脚本退出码是多少、输出内容是什么。这是排查问题的第一手段。
如果信息还不够,可以给脚本日志落盘。比如在 Python 脚本里把所有关键节点写入文件:
import datetime
def log(msg):
with open("/tmp/claude_hooks.log", "a") as f:
f.write(f"[{datetime.datetime.now()}] {msg}\n")然后每个函数的入口、关键判断、退出前都调用 log() 。这样即使 Claude Code 终端输出被刷掉,你还有一份完整的日志可以研究。
9.2 退出码与权限模式的坑
这里整理一下我实际使用中遇到的几个高发问题:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| Hooks 完全不触发 | Claude Code 处于 plan 模式 | 切换到 execute/accept 模式 |
| 脚本报错但 Claude Code 显示成功 | 脚本最后一条命令退出码为 0 | 显式调用 exit 0 或 sys.exit(0) |
| 每次工具调用都中断 | 钩子脚本异常返回了 1 | 检查脚本日志,修复异常;给脚本加 try-catch |
| 格式化后 AI 读取的内容是旧的 | PostToolUse 修改文件,但 AI 未重新读取 | 在提示词里要求 Claude 重新读取,或分两步执行 |
| 终端卡住很长时间 | 钩子里有同步网络请求 | 给请求设置超时,或用 nohup 后台执行 |
| 配置了但 Hook 没有在预期工具上触发 | matcher 写错 | 确认工具名正确,用 Bash* 通配测试 |
我遇到的最隐蔽的问题是 Python 脚本的 stdout 缓冲。脚本里 print 的内容,在某些环境下不会实时输出到 Claude Code 的终端,导致明明脚本说得清清楚“已拦截”,终端上却看不到。解决方法是在 Python 里加 sys.stdout.flush() ,或者运行命令时加 -u 参数: python3 -u script.py 。
9.3 环境变量注入与 shell 类型问题
最后一个坑是 shell 类型。Claude Code 在类 Unix 系统上默认使用 /bin/bash 执行命令,但 Hooks 脚本的命令部分用的是 sh -c 还是其他 shell,取决于具体实现。我碰到过一次现象:脚本在终端里手动跑完全正常,但作为 Hook 跑就报 command not found。排查后发现是 PATH 不一样——Hook 执行时 PATH 环境变量跟你的交互式终端不一样,很多工具(比如 black 、 isort )装在用户目录下的 ~/.local/bin 里,但 Hook 环境里这个目录不在 PATH。
最直接的解决方案是:脚本里设置绝对路径或显式导入 PATH:
os.environ["PATH"] = "/usr/local/bin:/usr/bin:/bin:~/.local/bin:" + os.environ.get("PATH", "")或者在 command 里直接指定:
{
"type": "command",
"command": "export PATH=$PATH:~/.local/bin && python3 .claude/hooks/format_file.py"
}10. 避坑指南与工作流建议
到了这一步,配置和调试其实你都已经能自己搞定了。但有几个我在实际项目中总结出来的经验,还是想单独拿出来说一说。
10.1 团队协作中 Hooks 的分发与维护
Hooks 脚本最好集中管理。不要把一堆脚本零散地放在 .claude/hooks/ 下面,时间长了,新成员都不知道这个文件是干嘛的。我建议在项目根目录建一个 claude-hooks/ 目录,然后写一个 README.md 说明每个文件的职责,再用一个 setup.sh 把脚本统一安装到 .claude/hooks/ 下。
如果你用的是 monorepo 多包结构,Hooks 配置还会涉及“在子目录里运行 Claude Code 时,配置文件怎么加载”的问题。目前的经验是,Claude Code 会在当前工作目录向上查找 .claude 目录,所以你在子项目里跑 Claude Code 时,应该能正确加载根目录的配置。不过,脚本内部的路径处理要基于 CLAUDE_PROJECT_DIR ,否则容易出错。
10.2 性能开销与钩子脚本的编写原则
Hooks 的执行是同步的,脚本跑多久,Claude Code 就等多个。如果你的 PostToolUse 钩子每次编辑后都要格式化整个项目,几分钟都跑不完,那 Claude Code 基本就没法用了。所以关于性能有三条原则:一是只做必要的事,能一次脚本搞定的不要拆成三个;二是尽量只处理影响范围内的文件;三是所有耗时操作(网络请求、全量扫描、模型推理)都丢到后台异步执行。
另一个原则是幂等性。同一个脚本反复执行,结果应该是一致的,不能因为有残留状态导致第二次执行走不同的分支。这在 Hooks 场景下非常重要,因为你不知道 Claude 会以什么顺序、什么频率调用工具,钩子可能在任何时刻被触发。
10.3 从安全到工程化的进阶路线
如果你想把 Hooks 用到更深,有一条可以进阶的路线:从简单的黑名单拦截,升级为完整的安全策略体系。比如用一个决策表来管理权限,而不再用硬编码的正则规则列表。把命令分类为“允许”“需人工确认”“禁止”三个等级,白名单之外的命令默认进入“需人工确认”等级,由人工在 Claude Code 的交互界面里确认。这套机制可以大幅提升 AI 编程的安全性,适合对安全要求比较高的金融、医疗、政企类项目。
更进一步,可以把 Hooks 的执行结果接入指标系统,比如统计每天有多少次危险命令被拦截、拦截率是多少、哪些模式频繁触发,用来优化安全策略。这就把 AI 编程助手真正纳入了可观测性监控的范围。不过这部分需要结合具体的监控后端来实现,就不展开写了。
总结
我个人把 Hooks 这套机制玩明白之后,最大的感受是:Claude Code 这个工具终于从一个“有多大能力就闯多大祸”的自由选手,变成了一个“能力边界由你定义”的可控工具。安全管控、自动化规范、通知集成、团队治理,全都靠这一套小机制串起来了。配置的时候别急着一次性上很多 Hooks,建议先挑一个场景(比如 PreToolUse 拦截危险命令)跑通,再慢慢叠加。脚本记得多写日志、多测退出码,尤其是 0 和 2 的区别,一定要吃透。等你真正用顺了,你会发现这个功能的想象空间比大多数教程里写的要大得多。
以上就是Claude Code Hooks完全指南:手把手教你拦截危险命令和自动化任务的详细内容,更多关于Claude Code Hooks使用教学的资料请关注脚本之家其它相关文章!
相关文章

Claude Code Hooks配置教程:从零实现任务完成提醒和代码自动检查
这篇文章将手把手教你用Hooks把规矩变成自动执行的流程,5个即拿即用的配置覆盖代码格式化、危险命令拦截、敏感文件保护、任务完成提醒和自动检查,从此AI干活不再漏事,效率2026-10-26
本文总结 Claude Code / 本仓库恢复版 claude-code 中 hooks 的类型、触发时机、典型使用场景、配置方式与注意事项, 下面就来详细的介绍一下,感兴趣的可以了解2026-06-04
Hooks 是用户自定义的脚本/命令,在 Claude Code 的特定生命周期节点自动执行,支持 4 种类型、4 个生命周期事件,下面小编就和大家详细介绍一下Hook这四种类型的具体使用吧2026-06-04
Claudede Hooks实战指南,涵盖三级拦截体系,两脚本开箱即用,覆盖致命、高风险、警告三级操作,通过配置settings.json灵活定制拦截策略,支持PowerShell与原生Toast通知,适用于2026-06-03
Claude Code 每次调用工具、等待输入、结束会话,都会触发对应的Hook生命周期事件,Hook 脚本除了做判断和记录,还可以把事件转发到本地 socket,让一个常驻进程处理所有状2026-06-02





最新评论