Claude Code 擅长写代码,但它本质上仍然是一个概率系统。你可以要求它在每次编辑文件后运行 Prettier。你可以把这个指令写进 CLAUDE.md。但有些时候,它就是会……忘记。
Claude Code hooks 解决的正是这个问题:在每次操作前、过程中和之后提供确定性的控制,让这些规则变成真正会被执行的流程。
过去几个月里,我在多个项目中使用了 hooks,它已经成为整个 Claude Code 工作流中不可或缺的一部分。本文会从基础讲起,带你搭建一套可以直接落地使用的配置。
Claude Code hooks 本质上就是一些你自己定义的操作,比如 shell 命令、HTTP 调用,或者额外的提示,它们会在 Claude Code 的不同阶段自动执行。
和写在提示词里的约定不一样,hooks 是一定会触发的,不会被「忘掉」。这也意味着你可以更稳定地控制一些事情,比如代码格式化、安全检查、通知,或者整个开发流程里的自动化步骤。
概率性问题
关键点在这里:CLAUDE.md 里的规则更像是建议,而不是强制执行的约束。
你可以写下“编辑 TypeScript 文件后总是运行 npx prettier –write”,大多数时候 Claude 会照做。但问题就在于——「大多数时候」是不够的。当你需要统一代码格式、阻止代码进入生产,或者记录关键操作时,这种不确定性就会变成隐患。
这其实是所有 AI 编码工具都会遇到的问题:Claude 本质上是语言模型,是基于概率工作的。你可以通过上下文去引导它,但没办法保证它每次都按规则执行。
Hooks 如何解决这个问题
Hooks 的思路很直接:不再依赖 LLM 的「记不记得」,而是直接在关键节点插入确定性的执行逻辑。
它们会在特定的生命周期阶段触发,比如工具运行前(PreToolUse)、运行后(PostToolUse)、收到通知时、会话开始,或者 Claude 停止时。可以把它理解成给 AI 编码流程加了一层「自动执行的钩子」,有点类似 Git hooks,只不过作用对象换成了 Claude。
目前主要有四种类型:
实际用下来,大部分场景用 command 就够了,能覆盖 90% 的需求。
Claude Code hooks 的执行流程其实很简单:当某个事件触发(比如 PreToolUse(Write))时,系统会先检查匹配规则,看有没有对应的 hook 需要执行。如果匹配上,就运行你的 hook 脚本,并通过 stdin 传入一段 JSON 数据。
脚本执行完之后,会根据退出码决定下一步:
- • 返回
0:继续执行原操作 - • 返回
2:直接阻止这次操作
不管你用的是哪种类型的 hook,这套流程都是一样的。
事件 → 匹配器 → Hook → 退出码(4步流程)
每个 hook 的执行流程其实就是这四步:
1. 事件触发 例如 PreToolUse(Write) |
- 匹配器检查 看 "Write" 是否命中 hook 的匹配规则 |
- Hook 执行 运行脚本,通过 stdin 收到 JSON |
- 退出码决定 0 = 放行 | 2 = 拦截 | 其他 = 报错
传给脚本的 JSON 里,会包含这次操作的完整信息,比如
tool_name、tool_input(文件路径、内容、命令)以及一些会话上下文。你只需要读这个 JSON,然后按自己的规则处理,最后返回一个退出码。在
PreToolUse这种场景里,退出码2特别有用:它可以直接拦下这次操作,并把你输出的内容作为反馈返回给 Claude。Claude 会根据这段反馈调整接下来的行为。配置作用域:用户、项目和本地
Hooks 可以放在三个层级的
settings.json中:实际用下来,最有价值的是 Project 这一层。把 hooks 写进
.claude/settings.json并提交到仓库后,团队里的每个人都会自动用上同一套规则,不需要再手动配置一遍,也更不容易出现环境不一致的问题。除了在
settings.json或插件中配置,hooks 也可以直接定义在 skills 和 subagents 里,通过 frontmatter 声明。这类 hooks 作用域仅限组件生命周期,并在执行结束后自动清理。注意:在 subagent 中定义的
Stophook,会自动转换为SubagentStop,因为子 agent 结束时触发的就是这个事件。if字段:更细粒度的过滤从 Claude Code v2.1.85 开始,hooks 支持
if字段,可以按参数过滤,而不只是看工具名称。比如以前你只能匹配Bash,那所有 Bash 命令都会触发;现在可以只针对像git push这样的特定命令生效:] }这个改动其实很关键。以前要么匹配太宽(所有 Bash 都触发),要么只能把判断逻辑写在脚本里,配置和实现混在一起,容易变乱。现在可以直接在配置层把范围收紧,逻辑更清晰,也更好维护。
JSON 输出:更精细的控制
除了退出码,hooks 还可以返回结构化 JSON,用来更精确地控制行为。
PreToolUse:执行前控制
, "additionalContext": "Context for Claude" } }常见字段说明:
- •
allow:直接放行(跳过权限检查) - •
deny:阻止执行,并说明原因 - •
ask:让用户确认 - •
updatedInput:在执行前修改参数(比如改命令) - •
additionalContext:给 Claude 补充信息
PermissionRequest:权限阶段控制
} } }Stop / SubagentStop:结果阶段约束
{ "decision": "block", "reason": "测试失败。请在完成测试前修复它们" }Claude Code 在整个运行过程中提供了 20 多种 hook 事件(可以在官方文档和 changelog 里查到完整列表)。但实际用下来,最常用的还是这几个:
PreToolUse、PostToolUse、Notification和Stop。 像ConfigChange、FileChanged这些新一点的事件,更适合做一些高级自动化。下面是一个常用事件的速查表:
大部分场景,其实用好
PreToolUse和PostToolUse就够了。如果再加一个SessionStart,基本可以覆盖大多数日常自动化需求。Claude Code 支持四种 hook 类型:
Command Hooks(主力军)
Command hooks 本质上就是执行一段 shell 命令,并通过退出码决定结果。执行时会通过 stdin 拿到一份 JSON,里面包含当前操作的所有信息。
{ "hooks": { "PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "jq -r ‘.tool_input.command’ | grep -q ‘rm -rf /’ && exit 2 || exit 0" }] }] } }大多数实际场景都会用到它——比如代码格式化、文件保护、通知、以及各种自动化流程。优点也很直接:快、简单、可控。
实用技巧:可以加上
async: true,让 hook 在后台执行,不阻塞 Claude 的流程,适合用在日志记录、通知这类不需要同步完成的操作。HTTP Hooks(外部集成)
HTTP hooks 会把当前事件以 JSON 的形式 POST 到一个 URL,然后根据返回的状态码决定结果。它很适合用来对接外部系统,比如发消息到 Slack、Discord、PagerDuty,或者接入你自己的监控和仪表板。如果需要更严格的控制,也可以在执行操作前先请求外部服务,让它来决定是否放行。
{ "hooks": { "Stop": [{ "matcher": "", "hooks": [{ "type": "http", "url": "https://your-api.com/claude-webhook" }] }] } }和 command hooks 不同,这里没有「退出码」,而是完全依赖 HTTP 状态码 + 响应内容。
关键区别:HTTP 状态码本身无法阻止操作,即使返回 4xx / 5xx,也只是记录错误,流程还是会继续。如果你真的想「拦住」某个操作,必须返回 2xx + 特定 JSON 结构:
}HTTP hooks 支持在 headers 里使用环境变量,但前提是:这些变量必须显式写在
allowedEnvVars里。这是为了避免无意中泄露敏感信息。{ "type": "http", "url": "https://hooks.example.com/validate", "headers": { "Authorization": "Bearer \(API_KEY", "X-Team-Id": "\)TEAM_ID" }, "allowedEnvVars": ["API_KEY", "TEAM_ID"] }有一个很容易忽略的点:未声明的变量会被解析为空字符串,不会报错。
Prompt Hooks(AI 驱动的决策)
Prompt hooks 会把当前事件的数据再交给 Claude,让它做一次简单判断(通常是放行还是阻止)。返回结果是一个 JSON,比如
"decision": "allow"或"block",同时带上简单的理由。{ "hooks": { "PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "prompt", "prompt": "这个 bash 命令在生产环境中运行安全吗?考虑:它是否修改系统文件、删除数据或访问敏感凭据?" }] }] } }这类 hook 要慎用——每触发一次,就相当于多跑了一次模型调用,既有延迟也有成本。
但在一些确实需要「判断」的场景下,它很有用,比如:
这种主观判断,用规则很难写清楚,用 prompt hook 反而更合适。另外,prompt hook 用的模型就是你当前会话里的模型,不需要单独配置。
Agent Hooks(工具辅助验证)
Agent hooks 会启动一个子 agent,这个子 agent 可以用 Read、Grep、Glob 等工具,在做决定之前先去读文件、查代码。
{ "hooks": { "PreToolUse": [{ "matcher": "Write", "hooks": [{ "type": "agent", "prompt": "检查正在写入的文件是否遵循项目的命名约定和导入模式。阅读 .claude/CONVENTIONS.md 了解规则。" }] }] } }可以把它理解成:在关键操作前,先让「另一个 Claude」帮你做一轮更深入的检查。
这也是最强的一种 hook,但代价也最大——更慢、更复杂。一般只在那种必须看上下文才能判断的高风险场景下使用,比如代码规范校验、复杂逻辑检查等。
在实际项目里,最常用的一些 hooks 基本都围绕这几件事:自动格式化(Prettier / Black)、保护关键文件、任务完成时发通知、会话开始时注入上下文、代码变更后跑测试、限制分支操作,以及记录所有工具使用。
下面的每个示例,都是可以直接用的
settings.json片段,放到.claude/settings.json就能生效。如果想看更多玩法,可以参考社区里的一些整理(比如 awesome-claude-code[1])。1. 自动格式化
{ "hooks": { "PostToolUse": [{ "matcher": "Write|Edit", "hooks": [{ "type": "command", "command": "FILE=\((jq -r '.tool_input.file_path // .tool_input.file' /dev/stdin); case "\)FILE" in .ts|.tsx|.js|.jsx) npx prettier –write "\(FILE" 2>/dev/null;; *.py) black "\)FILE" 2>/dev/null;; esac; exit 0" }] }] } }这个 hook 会在每次
Write或Edit后触发,从 stdin 的 JSON 里拿到文件路径,然后按文件类型运行对应的格式化工具。最后的
exit 0很关键——不管格式化有没有成功,都不会阻止 Claude 的操作。一般来说,格式化失败不应该打断流程。一个实用建议:如果你是多语言项目,可以顺手加上:
- •
.go→gofmt - •
.rs→rustfmt
2. 保护敏感文件
‘ && exit 2" }] }] } }关键点是
exit 2:它会阻止这次操作,并把你输出的 JSON 当作反馈返回给 Claude。Claude 收到后一般会改成提示你手动处理。if这一层也很重要——只在匹配这些文件时才触发,不会影响其他正常写入操作。3. 完成通知
] }] } }适用于 macOS(osascript)和 Linux(notify-send)。
matcher为空表示会匹配所有通知事件。你可以放心把长任务丢给 Claude,然后切去做别的事,等系统弹窗提醒你结果就行。4. 会话上下文注入
{ "hooks": { "SessionStart": [{ "matcher": "", "hooks": [{ "type": "command", "command": "echo ’{"message": "项目: ‘"\((basename \)(pwd))"’ | 分支: ‘"\((git branch --show-current 2>/dev/null || echo none)"' | 最后提交: '"\)(git log –oneline -1 2>/dev/null || echo none)"’"}‘; exit 0" }] }] } }这会把当前项目名、Git 分支和最近一次提交带入每个会话。Claude 会自动读取这些信息,不用你再手动说明当前在哪个分支上。
5. 自动运行测试
] }] } }如果检测到对应的测试文件存在,它会在 Claude 修改源代码后自动运行测试。
tail -5用来控制输出长度,避免日志太长,timeout则防止测试卡住或失控。这个配置和 AI 驱动的代码审查流程配合得很好。6. 分支保护强制执行(高级)
’ && exit 2" }] }] } }这会阻止任何对 main、master 或 production 分支的
git push操作。Claude 会收到失败反馈,并通常会改为建议你切到功能分支再提交。7. 安全审计日志(高级)
{ "hooks": { "PostToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "INPUT=\((cat /dev/stdin); CMD=\)(echo "\(INPUT" | jq -r '.tool_input.command'); echo "[\)(date -u +%Y-%m-%dT%H:%M:%SZ)] BASH: \(CMD" >> .claude/audit.log; exit 0" }] }] } }它会把 Claude 执行的每一条 Bash 命令记录到审计日志中,并附带 UTC 时间戳。这个日志在做安全审查或者回溯「当时到底执行了什么」时特别有用。一般建议把
.claude/audit.log加到.gitignore里,避免被提交到仓库。一个比较通用的入门配置,通常会包含这几类 hooks:
基本上可以用在每一个新项目中,可以根据具体技术栈做一点调整,但整体结构基本不变。
完整配置
{ "hooks": { "SessionStart": [{ "matcher": "", "hooks": [{ "type": "command", "command": "echo '{"message": "项目: '"\)(basename \((pwd))"' | 分支: '"\)(git branch –show-current 2>/dev/null)"‘ | Node: ’"\((node -v 2>/dev/null)"'"}'; exit 0" }] }], "PreToolUse": [' && exit 2" }] }], "PostToolUse": [{ "matcher": "Write|Edit", "hooks": [{ "type": "command", "command": "FILE=\)(jq -r ‘.tool_input.file_path // .tool_input.file’ /dev/stdin); case "\(FILE" in *.ts|*.tsx|*.js|*.jsx) npx prettier --write "\)FILE" 2>/dev/null;; *.py) black "\(FILE" 2>/dev/null;; *.go) gofmt -w "\)FILE" 2>/dev/null;; esac; exit 0" }] }], "Notification": [] }], "Stop": [{ "matcher": "", "hooks": [{ "type": "command", "command": "echo ‘[STOP] ’"\((date +%H:%M:%S)"'' >> .claude/session.log; exit 0" }] }] } }如何针对你的技术栈定制
把前面那些 hooks 示例里的格式化命令、测试命令和文件匹配规则,替换成你当前技术栈对应的版本就行了,整体结构基本不用动。
验证 Hooks 是否生效
可以用下面三种方式快速确认 hooks 有没有正常工作:
1.
/hooks命令在 Claude Code 里输入
/hooks,可以看到当前注册的所有 hooks,包括匹配器和启用状态。这是最直接的查看方式。2. 检查配置文件
直接查看配置:
- •
~/.claude/settings.json(用户级) - •
.claude/settings.json(项目级)
确认你的 hooks 是否真的写进去了、有没有拼写或结构问题。
3. 快速开关
在
settings.json里加上"disableAllHooks": true可以临时关闭所有 hooks(不用删配置)。需要恢复时,删掉这一行或改成false即可。Claude Code 的 hooks 在 Headless 模式(
claude -p)下同样可以工作,不过有一些需要注意的差异:- •
Notificationhooks 依然会触发,但在 CI 环境里更适合改成写日志,而不是发桌面通知 - •
PreToolUse返回退出码 2 时,可以直接中断流程,用于插入人工审核步骤
在实际项目中,通常会把 hooks 和 CI 一起用,比如在 GitHub Actions 里通过
anthropics/claude-code-action@v1跑自动化流程,把格式化、校验、拦截这些逻辑统一放进去。Headless 模式行为
在 Headless 模式下,有一个很容易被忽略但非常实用的点:当 PreToolUse 返回退出码 2 时,并不会只是简单失败,而是暂停整个流程,等待你用
--resume恢复。这相当于在 CI 流水线里加了一个「人工审批节点」,可以在关键操作前强制人工介入。GitHub Actions 集成
这是一个最小可用的 GitHub Actions 示例,用来在 CI 中运行 Claude Code:
- name: 运行 Claude Code uses: anthropics/claude-code-action@v1 with: prompt: "审查此 PR 并建议改进" allowed_tools: "Read,Grep,Glob" env: ANTHROPIC_API_KEY: \){{ secrets.ANTHROPIC_API_KEY }}你的
.claude/settings.json会跟着仓库一起进入 CI 环境,所以这些 hooks 在 CI 里的触发方式和本地是一样的。需要注意的是,如果某些 hooks 依赖本地环境(比如osascript这种桌面通知),最好加上兜底逻辑或者条件判断,避免在 CI 里出问题。团队 Hook 管理
在团队里,一个比较稳定的做法是按作用域分层管理 hooks:
- •
.claude/settings.json(提交到仓库) —— 放团队共享规则,比如文件保护、自动格式化、分支保护。这一层相当于「团队规范」,所有人都会生效。 - •
.claude/settings.local.json(gitignore)—— 放个人配置,比如通知方式、自定义日志、实验性 hooks,只对自己生效。 - •
~/.claude/settings.json(全局)—— 放跨项目的默认设置,比如通知风格、格式化偏好。
团队统一规则(类似
.editorconfig) + 本地个人配置(类似 IDE 设置)。当共享 hooks 固化在仓库里之后,「在我机器上没问题」的情况会明显减少。Hook 没有触发
Matcher 拼写问题:matcher 区分大小写,比如
"write"不会匹配Write。可以用/hooks查看实际的工具名称。JSON 格式错误:少一个逗号或括号,整个 hooks 可能直接失效,而且没有明显提示。可以用
jq . settings.json检查格式。被全局禁用了:检查是否存在:
"disableAllHooks": true,如果有,所有 hooks 都不会执行。Hook 运行但不阻止
退出码用错了:
exit 1表示「执行出错」,不是「阻止操作」。如果要拦截,必须用exit 2。这是最常见的坑。没有返回提示信息:阻止操作时,最好返回一段 JSON,让 Claude 知道原因:
echo ‘{"message": "阻止: 原因"}’无限循环
Stop hook 触发了新的操作:如果你的
Stophook 里又去写文件、执行命令,可能会再次触发 Claude 的流程,从而进入循环。建议Stophook 只做「被动操作」,比如记录日志、发送通知、清理资源。PostToolUse hook 引发二次编辑:如果
PostToolUsehook 修改了文件,就会再次触发PostToolUse,形成连锁反应。建议限制 matcher(只匹配特定文件或操作)或使用if条件避免重复触发。性能问题
SessionStart 太重:所有
SessionStarthooks 会在启动时同步执行,如果数量多或逻辑复杂,就会拖慢启动。建议使用轻量操作,每个最好在 1 秒内完成。热路径里跑重脚本:
PreToolUse和PostToolUse会频繁触发,如果里面有网络请求或重计算,很容易拖慢整体速度。建议给 hook 加timeout(毫秒)或者把逻辑拆到 HTTP hook / 后端服务里处理.没有做缓存:如果每次都重复做同样的检查(比如查当前分支、判断是否受保护),性能会很差。建议把结果缓存到临时文件,避免每次都跑一遍 Git 或其他命令。
引用链接
[1]awesome-claude-code: https://github.com/hesreallyhim/awesome-claude-code - •
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/270751.html