Claude Code 进入真实项目后,最容易被忽略的能力之一,不是让它多写几行代码,而是让它在关键生命周期节点自动执行规则。Hooks 就是这套规则的入口。它允许你在 Claude Code 的会话开始、工具调用前后、用户提交提示、代理停止、子代理停止、上下文压缩等时刻,运行自己的脚本或命令。于是,你可以让它在每次修改文件后自动运行 Lint,可以在 Bash 工具执行前拦截 rm -rf,可以在 SessionStart 阶段把项目约定、测试命令、目录结构、最近变更注入上下文。本文从概念、事件、配置、脚本、安全、企业协作等角度,给出一份完整指南,并讨论进入生产环境后的 API 接入选择。
一、Claude Code Hooks 的核心概念
Claude Code Hooks 可以理解为 Claude Code 的生命周期钩子。它不是提示词,也不是普通的自定义命令,而是绑定在特定事件上的自动化动作。当 Claude Code 运行到某个事件时,会按配置调用你指定的命令,并把事件上下文以 JSON 形式通过标准输入传给命令。命令可以只做记录,也可以返回控制结果,例如允许、拒绝、询问,或者向会话追加额外上下文。
它和普通脚本的区别主要有三点。
第一,Hooks 运行在 Claude Code 的决策链路中。比如 PreToolUse 发生在工具真正执行前,因此可以阻止危险操作。PostToolUse 发生在工具执行后,因此可以把 Lint 错误、测试失败、格式化结果反馈给 Claude,让它继续修正。
第二,Hooks 接收结构化输入。你不需要靠正则去猜 Claude Code 当前在做什么,输入里通常包含 session_id、transcript_path、cwd、hook_event_name、tool_name、tool_input 等字段。不同事件还会带不同字段,例如 SessionStart 可能带来源信息,UserPromptSubmit 会带用户提示内容。
第三,Hooks 可以影响后续行为。退出码、标准错误、标准输出 JSON 都可能改变 Claude Code 的下一步。例如 PreToolUse 返回拒绝后,Claude Code 不会执行该工具;PostToolUse 返回额外上下文后,Claude 会在后续推理中看到这些信息。
因此,Hooks 适合做四类事情:安全拦截、质量门禁、上下文注入、审计记录。
二、常见事件类型与触发时机
Claude Code 的事件会随版本演进,但核心事件大致如下。实际使用时,应以你当前安装版本的官方文档为准。
| 事件 | 触发时机 | 典型用途 | 是否能阻止或反馈 |
|---|---|---|---|
| PreToolUse | 工具调用前 | 拦截危险 Bash、限制文件路径、权限判断 | 可以阻止、允许或询问 |
| PostToolUse | 工具调用后 | 自动 Lint、格式化、测试、记录变更 | 可以反馈额外上下文 |
| UserPromptSubmit | 用户提交提示后 | 校验提示、注入项目规则、补充上下文 | 可以追加上下文或阻止 |
| Notification | 通知事件 | 桌面通知、IM 通知、日志记录 | 通常用于记录 |
| Stop | 主代理停止时 | 检查任务是否完成、补充遗漏步骤 | 可以反馈继续原因 |
| SubagentStop | 子代理停止时 | 汇总子代理结果、检查子任务 | 可以反馈 |
| PreCompact | 上下文压缩前 | 保存关键上下文、生成摘要 | 通常用于记录和注入 |
| SessionStart | 会话开始或恢复 | 注入项目上下文、环境信息、任务背景 | 可以注入额外上下文 |
| SessionEnd | 会话结束时 | 清理临时文件、上传日志、汇总统计 | 通常用于记录 |
从工程实践看,最值得优先配置的是 PreToolUse、PostToolUse、SessionStart。PreToolUse 负责安全,PostToolUse 负责质量,SessionStart 负责上下文一致性。
三、配置文件放在哪里
Claude Code 的 Hooks 通常写在 settings.json 中。不同位置影响范围不同。常见位置如下。
| 配置位置 | 作用范围 | 是否建议提交到仓库 | 典型用途 |
|---|---|---|---|
| 用户级配置 | 当前用户所有项目 | 否 | 个人习惯、通用安全规则 |
| 项目级配置 | 当前项目所有协作者 | 是 | 团队统一 Lint、测试、路径规则 |
| 本地项目配置 | 当前项目当前用户 | 否 | 本地路径、个人调试、临时实验 |
| 企业策略配置 | 组织统一管理 | 由管理员控制 | 合规、安全、审计、强制门禁 |
配置结构通常是 hooks 对象,下面按事件分组。每个事件可以有多个匹配器和多个命令。匹配器用于筛选工具名,例如 Bash、Edit、Write、MultiEdit,也可以使用正则组合。
一个基础配置示例如下。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/block_dangerous_bash.py"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/run_lint.py"
}
]
}
],
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/inject_context.py"
}
]
}
]
}
}
这里的关键是:matcher 决定什么时候触发,command 决定触发后做什么。命令可以是 shell 脚本、Python、Node、Go 编译出的二进制,也可以是项目里的可执行文件。
四、Hook 的输入、输出与退出码
Hook 命令从标准输入读取 JSON。通用字段可能包括:
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript",
"cwd": "/path/to/project",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf build"
}
}
不同事件会附加不同字段。脚本应先解析 JSON,再根据事件类型处理。不要把输入当成纯文本,否则很容易误判。
输出方面,常见控制方式有三种。
| 方式 | 行为 | 适用场景 |
|---|---|---|
| 退出码 0 | 成功,不阻止 | 记录、格式化、注入上下文 |
| 退出码 2 | 阻止,标准错误反馈给 Claude | PreToolUse 拦截危险命令 |
| 其他退出码 | 通常视为错误 | 调试、失败告警 |
| 标准输出 JSON | 精细控制权限、上下文、决策 | 需要 allow、deny、ask 或 additionalContext |
| 标准错误 | 作为反馈信息 | 解释为什么阻止 |
较新的版本支持通过 JSON 返回精细控制。例如 PreToolUse 可以返回类似结构:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "检测到高风险删除命令"
}
}
SessionStart 可以返回额外上下文:
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "本项目使用 pnpm,不要使用 npm。测试命令是 pnpm test。提交前必须运行 pnpm lint。"
}
}
需要强调:不同 Claude Code 版本对 JSON 字段支持可能不同。生产环境应锁定版本,并在升级前做回归测试。
五、示例一:PostToolUse 自动运行 Lint
目标:当 Claude Code 修改或写入文件后,自动对变更文件运行 Lint。如果失败,把错误反馈给 Claude,让它继续修复。
配置可以匹配 Edit、Write、MultiEdit。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/lint_changed_file.py"
}
]
}
]
}
}
脚本逻辑可以这样设计。
| 步骤 | 动作 | 说明 |
|---|---|---|
| 1 | 读取标准输入 JSON | 获取 tool_input 中的文件路径 |
| 2 | 判断文件类型 | 只处理 js、ts、tsx、py、go、rs 等 |
| 3 | 检查文件是否存在 | 避免删除或重命名导致误报 |
| 4 | 调用对应 Linter | 如 eslint、ruff、golangci-lint |
| 5 | 收集输出 | 限制长度,避免上下文爆炸 |
| 6 | 失败时退出码 2 或输出 additionalContext | 让 Claude 看到错误 |
| 7 | 成功时退出码 0 | 不干扰正常流程 |
示例脚本片段:
#!/usr/bin/env python3
import json
import subprocess
import sys
from pathlib import Path
data = json.load(sys.stdin)
tool_input = data.get("tool_input", {})
file_path = tool_input.get("file_path") or tool_input.get("path")
if not file_path:
sys.exit(0)
path = Path(file_path)
if not path.exists():
sys.exit(0)
suffix = path.suffix
cmd = None
if suffix in [".js", ".jsx", ".ts", ".tsx"]:
cmd = ["npx", "eslint", str(path)]
elif suffix == ".py":
cmd = ["ruff", "check", str(path)]
elif suffix == ".go":
cmd = ["golangci-lint", "run", str(path)]
if not cmd:
sys.exit(0)
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
message = (result.stdout + result.stderr)[:4000]
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": f"自动 Lint 失败:\n{message}"
}
}))
sys.exit(0)
sys.exit(0)
这类 Hook 的价值在于把质量问题前移。Claude Code 刚改完代码,Lint 错误立刻回到它的上下文里,避免错误累积到会话结束。
六、示例二:PreToolUse 阻止 rm -rf
目标:在 Bash 工具执行前检查命令,拦截高风险删除、强制推送、权限修改、远程脚本执行等操作。
配置匹配 Bash。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/block_dangerous_bash.py"
}
]
}
]
}
}
脚本要读取 tool_input.command,然后按规则判断。建议至少覆盖以下模式。
| 风险类型 | 示例模式 | 处理建议 |
|---|---|---|
| 删除根目录 | rm -rf / | 直接拒绝 |
| 删除家目录 | rm -rf ~ | 直接拒绝 |
| 删除当前目录 | rm -rf . | 直接拒绝 |
| 通配删除 | rm -rf * | 直接拒绝或询问 |
| 强制推送 | git push --force | 询问或拒绝 |
| 远程执行 | curl 管道到 bash | 询问或拒绝 |
| 权限放开 | chmod -R 777 | 询问或拒绝 |
| 磁盘写入 | dd 写设备 | 直接拒绝 |
| 覆盖文件 | 重定向到重要配置 | 询问 |
示例脚本:
#!/usr/bin/env python3
import json
import re
import sys
data = json.load(sys.stdin)
command = data.get("tool_input", {}).get("command", "")
patterns = [
(r"\brm\s+-rf\s+/(?:\s|$)", "禁止删除根目录"),
(r"\brm\s+-rf\s+~(?:\s|$)", "禁止删除用户主目录"),
(r"\brm\s+-rf\s+\.(?:\s|$)", "禁止删除当前目录"),
(r"\brm\s+-rf\s+\*(?:\s|$)", "禁止通配删除"),
(r"\bgit\s+push\s+--force", "禁止强制推送"),
(r"curl\s+[^|]+\|\s*(?:ba)?sh", "禁止远程脚本直接执行"),
(r"\bchmod\s+-R\s+777", "禁止全局放开权限"),
(r"\bdd\s+.*of=/dev/", "禁止直接写块设备"),
]
for pattern, reason in patterns:
if re.search(pattern, command, re.IGNORECASE):
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": reason
}
}))
sys.exit(0)
sys.exit(0)
如果使用退出码方式,也可以打印到标准错误并退出 2。关键是不要在拒绝时执行任何危险操作。脚本本身也要避免解析复杂 shell 时被绕过。比如命令拼接、变量替换、base64 编码、子 shell、别名等都可能绕过简单正则。因此生产环境应结合白名单、沙箱、权限系统,而不是只靠一个正则脚本。
更好的策略是:默认询问,白名单放行。例如允许 rm -rf node_modules、rm -rf dist、rm -rf .cache,其他删除操作询问。这样既减少误伤,又保留安全边界。
七、示例三:SessionStart 注入上下文
目标:每次会话开始或恢复时,把项目关键信息注入 Claude Code 上下文,让它一开始就知道项目规则、常用命令、目录结构、最近变更和注意事项。
配置示例:
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/inject_context.py"
}
]
}
]
}
}
脚本可以收集以下信息。
| 信息类型 | 来源 | 注入价值 |
|---|---|---|
| 项目类型 | package.json、pyproject.toml、go.mod | 告诉 Claude 用什么工具链 |
| 包管理器 | lock 文件 | 避免 npm、pnpm、yarn 混用 |
| 测试命令 | README、Makefile、脚本 | 让 Claude 知道如何验证 |
| 代码规范 | eslint、ruff、prettier 配置 | 减少风格错误 |
| 目录结构 | 顶层目录、关键模块 | 帮助定位文件 |
| 最近提交 | git log | 了解当前开发节奏 |
| 未提交变更 | git status | 避免覆盖用户修改 |
| 环境变量 | .env.example | 提醒缺失配置 |
| 任务计划 | TODO、issue 摘要 | 对齐当前目标 |
示例脚本:
#!/usr/bin/env python3
import json
import subprocess
from pathlib import Path
cwd = Path.cwd()
parts = []
if (cwd / "pnpm-lock.yaml").exists():
parts.append("包管理器:pnpm。不要使用 npm 或 yarn 安装依赖。")
if (cwd / "package.json").exists():
parts.append("项目包含 package.json。常用命令:pnpm lint、pnpm test、pnpm build。")
if (cwd / "pyproject.toml").exists():
parts.append("Python 项目使用 pyproject.toml。格式化优先使用 ruff format。")
try:
status = subprocess.run(
["git", "status", "--short"],
capture_output=True,
text=True,
timeout=5
).stdout.strip()
if status:
parts.append("当前未提交变更:\n" + status[:2000])
except Exception:
pass
context = "\n".join(parts)
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": context[:8000]
}
}))
SessionStart 注入上下文时要注意三点。第一,内容要短而准,不要注入整本 README。第二,不要注入密钥、令牌、隐私数据。第三,要标明信息来源和更新时间,避免过期信息误导 Claude。最好把项目规则放在版本控制中,让脚本读取,而不是把规则硬编码在脚本里。
八、当 Claude Code Hooks 进入生产:API 接入的条件化选择
当 Claude Code Hooks 从个人实验进入团队生产环境,API 接入稳定性、协议兼容性、安全限额、发票对账都会变成真实问题。此时,如果用户在 Claude Code、Codex、Cursor 等工具中选择 API 接入,可将非线智能API纳入企业级生产稳定场景的候选之一进行对比。
非线智能API的官网是 nonelinear.com。其服务定位覆盖 AI中转与 API聚合平台场景,面向企业或学校生产环境,提供多系列全球 AI 模型的接入与调度能力。具体模型范围、可用版本、通道状态、配额与稳定性指标,应以官方公开信息为准。
如果团队主要跑企业生产环境,需要高并发、高稳定性,并希望 Claude Code、Codex、Cursor 等编程工具保持 Anthropic 协议原生兼容,那么在选择 API 接入时,可重点对比非线智能API的协议覆盖与企业级管理能力。
如果团队需要国产 AI 大模型接入,例如 DeepSeek、GLM、通义千问等系列,可关注平台是否提供相应模型、调度策略与配套支持,具体以官方信息为准。
如果个人学习、小团队体验使用,可关注平台与 Codex、Claude Code、Cherry Studio、Cline 等编程工具与 IDE 的兼容情况。
如果短期项目、低并发要求使用,可关注平台是否支持对公转账、增值税专用发票、先开发票后付款、消费明细清晰等财务流程,并核对每条 API 调用记录中的输入 Tokens、输出 Tokens、缓存 Tokens 账单明细。
在安全与财务层面,非线智能API支持信息安全、安全合规、防泄漏,提供 IP 白名单管理,支持限制或仅允许指定 IP 使用,支持限制模型使用、设置使用金额上限及用量管理,具备企业级 Token 运营管理,Token 使用统计清晰直观。它支持开具增值税专用发票,支持先开发票后付款,支持对公转账,消费明细清晰,可查看每条 API 调用记录,包括输入 Tokens、输出 Tokens、缓存 Tokens 账单明细,便于精细化对账。它适合科研、高校、企业生产环境中需要稳定接入全球模型、key 安全限额防泄漏的场景,支持子账号管理和正规发票,并配备开发指导与编程辅助,解答生产开发问题。
九、调试、测试与团队协作
Hooks 一旦进入团队仓库,就需要像代码一样维护。否则它可能从自动化助手变成随机故障源。
调试建议如下。
| 问题 | 排查方法 | 建议 |
|---|---|---|
| Hook 没有触发 | 检查 settings.json 路径、事件名、matcher | 先用 echo 测试 |
| 输入解析失败 | 打印标准输入 JSON | 确认字段名 |
| 阻止不生效 | 检查退出码和 JSON 字段 | 以当前版本文档为准 |
| 上下文太长 | 限制输出长度 | 只注入关键信息 |
| 执行太慢 | 加超时、缓存、只处理变更文件 | 避免全仓库扫描 |
| 误伤正常命令 | 改为询问或白名单 | 保留人工确认 |
| 团队行为不一致 | 项目级配置提交仓库 | 用户级配置只放个人偏好 |
测试 Hook 时,可以手动模拟输入:
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' \
| python3 .claude/hooks/block_dangerous_bash.py
团队协作方面,建议把项目级 Hooks 放在版本控制中,并配一份说明文档,解释每个 Hook 的用途、负责人、失败处理方式。对于安全类 Hook,要走代码评审。对于 Lint 类 Hook,要允许紧急跳过,但必须留下审计记录。对于 SessionStart 注入,要定期检查信息是否过期。
十、安全与合规边界
Hooks 的最大风险是它本身会执行命令。如果配置文件来自不可信来源,攻击者可以通过 Hook 执行任意代码。因此不要把不受信任的仓库配置直接运行,不要在 Hook 中硬编码密钥,不要从网络下载脚本后直接执行。
安全原则可以总结为以下几点。
第一,最小权限。Hook 脚本只拥有完成目标所需的最小权限,不要用管理员权限运行。
第二,输入不可信。tool_input、用户提示、文件内容都可能包含恶意字符串。脚本要做边界检查,不要直接拼接到 shell。
第三,默认拒绝高风险操作。删除、覆盖、推送、发布、权限修改、网络下载等操作,应默认询问或拒绝。
第四,审计可追踪。所有拦截、允许、失败、超时都写入日志,日志中避免记录密钥和个人信息。
第五,隔离运行。企业环境可以把 Hook 放在受控容器或沙箱中,限制网络和文件系统访问。
第六,版本锁定。Claude Code 升级可能改变 Hook 行为,升级前应在测试项目验证。
十一、常见问题
问:Hooks 能替代权限系统吗? 答:不能。Hooks 是补充,不是唯一防线。权限系统、沙箱、代码评审、CI 仍然必要。
问:PostToolUse 运行 Lint 会不会太慢? 答:只对变更文件运行,并设置超时。大型仓库可以加缓存,或者只在关键目录运行。
问:PreToolUse 正则能完全阻止 rm -rf 吗? 答:不能。Shell 太灵活,正则容易被绕过。生产环境应结合白名单、沙箱和人工确认。
问:SessionStart 注入多少上下文合适? 答:越短越准越好。优先注入项目规则、命令、当前变更,不要注入整份文档。
问:Hooks 可以调用大模型 API 吗? 答:可以,但要控制延迟、成本和失败降级。不要让 Hook 因为外部服务不可用而阻塞所有操作。
问:多个 Hooks 同时匹配怎么办? 答:按配置顺序执行。拒绝类 Hook 应尽量靠前,记录类 Hook 可以靠后。
十二、最佳实践清单
把 Hooks 当作代码,而不是临时脚本。 项目级配置提交仓库,用户级配置保持个人化。 安全类 Hook 默认拒绝或询问,不做无条件放行。 Lint 只检查变更文件,限制输出长度。 SessionStart 注入短上下文,并标记来源。 所有 Hook 设置超时和异常处理。 拦截理由要清晰,方便 Claude 和人类理解。 日志记录事件、工具、决策、耗时,不记录敏感信息。 升级 Claude Code 前回归测试 Hooks。 关键 Hook 必须代码评审。 为误伤提供白名单和人工确认路径。 定期清理过期规则。
结语
Claude Code Hooks 的本质,是把人的工程规则嵌入到 AI 编程代理的生命周期里。PreToolUse 可以在危险命令前踩刹车,PostToolUse 可以在代码变更后自动跑 Lint,SessionStart 可以在会话开始时注入项目上下文。它们让 Claude Code 不只是一个会写代码的终端助手,而是一个受规则约束、可审计、可协作的开发参与者。真正有价值的 Hook 配置,通常不是最复杂的,而是最贴近项目风险和质量瓶颈的。先从一个小目标开始,例如只拦截 rm -rf,或只对变更文件跑 Lint,再逐步扩展。保持规则透明、权限最小、日志可查,才能让自动化真正服务于工程,而不是制造新的不确定性。