去年我们在用 Claude Code 跑内部基建的时候,经常遇到这样一个窒息场景:AI 哐哐一顿输出,看着挺像那么回事,结果 CI/CD 一跑直接炸了。为了这事,团队没少在审查 AI 写的烂代码上掉头发。
那时候我们才意识到,光写 Commands 和 Skills 不够------它们只是告诉 AI "你可以做什么"。如果想强制告诉它"这件事不准做",或者"代码测试没通过,不准停下来继续改",你最需要的其实是 Hooks(钩子)。
本质上,Hook 就是 AI 时代的中间件,负责拦截和增强每一次大模型与本地环境的交互。
钩从何来:AI 的动作在哪能被拦截?
当我们和 Claude Code 对话时,其实就是一条"发起请求 -> 内部执行 -> 收尾"的流水线。在这个过程中,官方提供了大大小小几十个事件可以挂载 Hook。
具体可以看看官方白皮书 Claude Code Hooks Reference。实战中,我们大体只围绕三个核心层级做埋点:
1. 抓执行源头(会话与回合级)
这类事件通常用来做前置的环境准备或者准入检查。
SessionStart:新会话刚起来,顺手帮你加载一下环境变量。UserPromptSubmit:在你敲下回车,但还没发给大模型前。如果你想给团队搞个"敏感词或者违规操作"的硬拦截,挂在这里刚刚好。
2. 抓执行过程(工具级)
AI 在干活(Agentic Loop)时,会高频调用极多工具。这是最容易产生破坏乱动的地方。
PreToolUse:真正的执行前闸门。要不要放行?要不要人工审批?全在这步拦截。这是防范 AI 删库跑路的第一道防线。PostToolUse/PostToolUseFailure:典型的执行后观察点。这会儿木已成舟改变不了结果,用来做行为审计和打日志最合适。
3. 抓收尾环节(回合结束)
这绝对是我最喜欢的一个节点,专门用来治 AI "敷衍了事"的毛病。
Stop/SubagentStop:代表 AI 以为自己干完活了,准备交差了。这可是卡"质量门禁"的黄金时机。你完全可以在这塞个测试脚本,没过就直接把它"拒收",让它重写去。
怎么拦?四种武器大比拼
知道了能在哪里埋钩子,接下来就是"怎么拦"。目前在配置文件中(如 settings.local.json),主要有四种类型的武器可以选。到底用哪种,取决于你的业务场景有多复杂。
简单粗暴:Command 类型
这是我写得最多的一种,本质上就是触发一个本地的 Shell 脚本或者 Node 命令。比如卡质量门禁,跑个 ESLint 检查一下。
json
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/stop-quality-gate.mjs",
"timeout": 120,
"statusMessage": "稍微等下,我在跑 ESLint..."
}
]
}
]
}
}
小贴士:默认超时只有 60 秒。如果有跑测试的场景,记得把
timeout拉长,不然会直接因为超时中断。
AI 魔法打败魔法:Prompt 类型
有时候你没法用准确的代码(比如正则)去圈定报错。比如你想校验"它的变量命名准不准业务语义",那就干脆外包给 LLM 自己评判。这相当于雇了个小裁判。
json
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"promptTemplate": "查一下这段代码,给 1-10 分,如果不达标建议是什么。\n\n内容:\n{output}",
"timeout": 30
}
]
}
]
}
}
挂载小弟出马:Agent 类型
这是最重的一招。比如当主分类器想调用高风险环境变更时(触发 PreToolUse),你可以当场通过 Hook 拉起一个子 Agent。这哥们儿还能用 Grep 去全库查代码线索,查完了再决定通不通过。
跨系统集成:API 类型
很多大厂都有自己的中台规范服务。当你敲下一行 Prompt 准备提交时(UserPromptSubmit),可以通过 API 发个请求给公司后端的安全网关。这招ToG的团队特别好使。
压箱底实战:手搓一个"改错永动机"
纸上得来终觉浅。推荐一个落地的姿势,Stop 里挂个代码检测。
核心逻辑就是:AI 写完了 -> 触发代码检测脚本 -> 报错了 -> 脚本丢出 decision: "block" 并附带 reason -> AI 收到错误后没脸走,只能默默接茬干活 -> 修复完再次检测 -> 一直循环到全绿。这就是个不知疲倦的帕鲁。
示例门禁代码(存为 stop-quality-gate.mjs):
javascript
#!/usr/bin/env node
import fs from "node:fs";
import { spawnSync } from "node:child_process";
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
// 把终端命令行封装下
function runCheck(cwd, command, args) {
const result = spawnSync(command, args, {
cwd,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
return {
ok: result.status === 0,
output: [result.stdout || "", result.stderr || ""].join("\n").trim(),
};
}
// 核心打卡函数,不让 AI 走
function block(reason) {
process.stdout.write(
JSON.stringify({ decision: "block", reason, systemMessage: reason })
);
process.exit(0);
}
// 拿到 Hook 传过来的上下文防崩溃设定
let payload = {};
try {
const rawInput = fs.readFileSync(0, "utf8");
payload = rawInput ? JSON.parse(rawInput) : {};
} catch {
block("Stop hook JSON 解析炸了,检查一下语法。");
}
// ⚠️ 极其关键的安全舱:别让它死脑筋陷进无限循环,比如这语法就是 AI 跨不过去的坎
if (payload.stop_hook_active) {
process.stdout.write(JSON.stringify({ systemMessage: "二次拦截,强行放行避坑" }));
process.exit(0);
}
// 假装去捞了本次变动的文件(省略 Git 工具函数细节)
const changedFiles = ["src/main.ts"];
const lintResult = runCheck(projectDir, "npm", ["run", "lint"]);
if (!lintResult.ok) {
block(`老哥,检查出错了,修好了再结束:\n\n${lintResult.output.slice(0, 800)}`);
}
process.stdout.write(JSON.stringify({ systemMessage: "测试全绿通过" }));
process.exit(0);
你只需要把代码同理扩展一下加上 TypeScript 类型检查或者 Jest,晚上就在旁边看着终端里 AI 一遍遍在那试错修 Bug。老实说,挺费Token的。
tips: 防止无效循环的关键是
stop_hook_active这个 flag。第一次触发时它不存在,脚本正常执行;如果检测到这个 flag 已经存在了,就说明是二次触发了,这时候直接放行避免死循环。 查看hook 执行情况可以通过:1. /debug 执行失败的错误信息。2. /hooks 查看是否加载了预期的 hook。 3. 自定义日志文件 4.给 hook 加 statusMessage 5. Command hook 的 stderr 输出会显示给用户(非阻塞),可以用来确认触发:echo "Hook triggered at $(date)" >&2
组合使用
1. 多钩子串联拦截
一个事件如果挂了多个 Hook,它是严格按顺序触发的流水线。 比如你的前置 Hook 负责"静态漏洞扫描",后置负责"单元测试"。只要漏扫炸了,那压根不会进行耗时的单元测试。极其适合那些重型开发流。 例如:
json
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/static-scan.mjs",
"timeout": 60,
"statusMessage": "静态扫描一中..."
},
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/unit-test.mjs",
"timeout": 120,
"statusMessage": "单元测试中..."
}
]
}
]
}
}
2. 局部限制的作用域(Frontmatter Hooks)
如果你不想全局所有会话都拦截,只需要在某些特定 Skill 或者 Subagent 里起效,你可以把 Hook 定义在它们的 YAML Frontmatter 头里面(官方示例):
yaml
---
name: code-reviewer
description: Review code changes with automatic linting
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "./scripts/validate-command.sh $TOOL_INPUT"
PostToolUse:
- matcher: "Edit|Write"
hooks:
- type: command
command: "./scripts/run-linter.sh"
---
相比于全局的 Settings Hooks,这类钩子的好处是随插随拔。这个子探员干完活休眠了,Hook 也就跟着一起清理掉了,不影响全局环境的干净。
总结起来就是一句话:在大模型自动驾驶时代,给 AI 安装刹车和离合机制,远比不停优化提示词更重要。只有把最坏的情况托底兜住(不会提交不可运行的代码),AI Agent 的能力上限才能真正被放出来发挥。