Hooks 怎么用?5 个能直接抄的自动化配置
技术系列第 2 篇。上一篇《CLAUDE.md 怎么写》说过:CLAUDE.md 是说明,权限和 Hook 才是门锁。这篇就把"锁"装上------5 个拿来即用的 hook 配置。

一、为什么需要 Hooks
假设你正用 Claude Code 维护一个项目。它干活利落,但有些事你每次都要叮嘱:改完记得格式化、别碰 .env、删库命令悠着点、写完做下检查。可它是 AI 不是员工,叮嘱不进考核:漏一次,格式乱了;漏两次,.env 里的密钥可能已经被读走。
这类"每次都必须发生"的事,靠反复口头强调不靠谱,写进 CLAUDE.md 也只是"说明",模型可能遵循也可能忘。稳的办法是把它变成流程:让 Claude Code 在特定动作前后自动执行你指定的命令。这就是 Hooks------你在关键动作上挂一段命令,到点自动执行,执行结果还能反过来影响流程。从此规矩不靠自觉,靠机制兜底。

二、配置写在哪、长什么样
Hooks 不写在 CLAUDE.md 里,而是写在 settings.json 中,共三个位置:
- 项目级
.claude/settings.json:随项目提交进 git,全组共享 - 个人级
~/.claude/settings.json:对你的所有项目生效 - 本地级
.claude/settings.local.json:不提交,适合先拿来做试验

配置结构是固定的三层嵌套:
json
{
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{ "type": "command",
"command": "python .claude/hooks/fmt.py" }]
}]
}
}

对应关系记三点:
- 事件名决定"什么时候触发"。常用的有:PreToolUse(工具执行前,可拦截)、PostToolUse(工具执行后)、UserPromptSubmit(你提交提示词时)、Stop(一轮回答结束时)、SessionStart(会话启动)、Notification(需要你注意时)。事件不止这些,以官方文档为准。
- matcher 决定"对哪些工具生效",写正则,如
Edit|Write、Bash。只有 PreToolUse 和 PostToolUse 需要 matcher;Stop 这类事件与具体工具无关,不用写。 - command 是真正执行的 shell 命令。建议 JSON 里只留一行
python 某脚本.py,逻辑全放脚本文件里------转义省心,审查也方便。

三、工作原理:事件、匹配、执行、退出码
一次工具调用会走完这样一条时间线,hook 在其中两个节点出手:

hook 命令启动时,Claude Code 会把一段 JSON 从标准输入喂给它:Bash 的命令在 tool_input.command,Edit/Write 的目标路径在 tool_input.file_path。比如你想拦一条危险命令,脚本实际收到的大致长这样(有删减):
json
{ "tool_name": "Bash",
"tool_input": { "command": "git push --force origin main" } }
脚本解析输入、做出判断,再用退出码表态------0 放行,2 阻断(stderr 的内容会反馈给 Claude),其他非零是非阻断错误,流程照常继续。更复杂的 JSON 输出控制方式本文不展开,以官方文档为准。记住退出码这个协议,下面的配置就都能看懂了。

四、5 个能直接抄的配置
先看全景,每一行都对应下文一节:

约定:脚本统一放在项目的 .claude/hooks/ 目录下。Windows 用户注意,路径含空格时 command 里的路径要加引号。安装任何一条都是同样五步:建目录、存脚本、把配置粘进 settings.json、会话里输入 /hooks 确认加载、随手找个该触发的地方试一把。
① 改完代码自动格式化
json
{
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{ "type": "command",
"command": "python .claude/hooks/format_changed.py" }]
}]
}
}
lua
import json, os, shutil, subprocess, sys
PRETTY = ["npx", "prettier", "--write"]
FORMATTERS = {".py": ["python", "-m", "black", "--quiet"], ".ts": PRETTY, ".js": PRETTY,
".jsx": PRETTY, ".tsx": PRETTY, ".css": PRETTY, ".html": PRETTY}
def main() -> int:
data = json.load(sys.stdin)
path = data.get("tool_input", {}).get("file_path", "")
cmd = FORMATTERS.get(os.path.splitext(path)[1].lower())
if not cmd or not os.path.isfile(path):
return 0 # 非代码文件或路径不存在:安全跳过
if shutil.which(cmd[0]) is None:
return 0 # 未安装格式化工具:跳过,不报错
subprocess.run(cmd + [path])
return 0
sys.exit(main())
从标准输入取 file_path,按扩展名挑格式化命令。适用:任何多人协作或在意风格的项目。注意两点:没装的格式化工具会被安全跳过,所以先把 black 或 prettier 装好;格式化失败不返回 2,避免格式问题拦死流程。
② 拦截危险命令
json
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{ "type": "command",
"command": "python .claude/hooks/block_danger.py" }]
}]
}
}
python
import json, re, sys
RULES = [
(r"\brm\s+(-[a-z]*r[a-z]*f|-[a-z]*f[a-z]*r)", "递归强制删除(rm -rf)"),
(r"\bgit\s+push\s+.*(--force|-f\b|--force-with-lease)", "强制推送远程分支"),
(r"\bdrop\s+table\b|\btruncate\s+table\b", "清空数据库表"),
(r"\bmkfs\b|\bdd\s+if=", "格式化/覆写磁盘"),
]
def main() -> int:
sys.stderr.reconfigure(encoding="utf-8") # 防止中文 Windows 下乱码
raw = json.load(sys.stdin).get("tool_input", {}).get("command", "")
cmd = re.sub(r"\s+", " ", raw.lower()) # 大小写、多余空格先归一化
for pat, why in RULES:
if re.search(pat, cmd):
print(f"已拦截:{why}", file=sys.stderr)
return 2 # 退出码 2 = 阻断,stderr 会反馈给 Claude
return 0
sys.exit(main())
命中黑名单就 exit 2,Claude 当场收到拒绝原因,会换个安全做法。适用:怕误删数据、怕强推覆盖同事提交的日常开发。注意:这是关键词匹配不是完整解析,绕过写法防不住,名单请按项目自己增删,它挡的是"手滑"不是"故意"。
③ 保护敏感文件
json
{
"hooks": {
"PreToolUse": [{
"matcher": "Edit|Write|Read",
"hooks": [{ "type": "command",
"command": "python .claude/hooks/guard_secret.py" }]
}]
}
}
lua
import json, os, sys
BAD_ENDS = (".pem", ".key", ".p12", ".pfx")
BAD_NAMES = ("id_rsa", "id_ed25519", "id_ecdsa")
def is_sensitive(path: str) -> bool:
name = os.path.basename(path).lower()
if name == ".env" or name.startswith(".env."):
return True # .env、.env.local 等变体都算
return name in BAD_NAMES or name.startswith("id_rsa.") or name.endswith(BAD_ENDS)
def main() -> int:
sys.stderr.reconfigure(encoding="utf-8") # 防止中文 Windows 下乱码
p = json.load(sys.stdin).get("tool_input", {}).get("file_path", "")
if is_sensitive(p):
print(f"已拦截:{p} 疑似密钥或环境变量文件", file=sys.stderr)
return 2
return 0
sys.exit(main())
判断不盯全路径盯文件名,所以 .env.local、config/.env.production 这类变体也逃不掉。适用:仓库里有真实密钥、或想让 AI 连读都不读密钥的场景。注意:env.example 这种示例文件不会被误伤;名单是保守起点,按需增删。
④ 任务完成提醒
css
{
"hooks": {
"Stop": [{
"hooks": [{ "type": "command",
"command": "powershell -c "[console]::beep(880,200)"" }]
}]
}
}
这条不需要脚本,一行命令即可。Stop 没有 matcher,Claude 每答完一轮就响一声。其他系统换个命令:macOS 用 osascript -e 'display notification "任务完成" with title "Claude Code"',Linux 用 notify-send "Claude Code" "任务完成"------我只在 Windows 实测过响铃,其余以你的环境为准。适用:挂着长任务切去做别的事的人。注意:别用弹窗类命令,太吵的提醒活不过三天。
⑤ 改完自动跑检查
json
{
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{ "type": "command",
"command": "python .claude/hooks/check_changed.py" }]
}]
}
}
sql
import json, os, subprocess, sys
CHECK = ["python", "-m", "py_compile"] # 换成你项目的快速检查,如 npx tsc --noEmit
def main() -> int:
sys.stderr.reconfigure(encoding="utf-8") # 防止中文 Windows 下乱码
path = json.load(sys.stdin).get("tool_input", {}).get("file_path", "").replace("\", "/")
if "/src/" not in f"/{path}" or not os.path.isfile(path):
return 0 # 只看 src 下的改动,其余跳过
r = subprocess.run(CHECK + [path], capture_output=True, text=True, encoding="utf-8", errors="replace")
if r.returncode:
print(r.stderr[-1200:], file=sys.stderr)
return 2 # 检查不过:让 Claude 直接看到报错
return 0
sys.exit(main())
检查失败就 exit 2,报错文本直接进 Claude 的眼睛,它下一轮就自己修,不用你当传话筒。适用:TypeScript、Python 这类有秒级检查手段的项目。注意:必须快------PostToolUse 是同步的,hook 慢一秒,每次修改都慢一秒;所以只跑单文件检查、且只对 src 目录生效,整包测试留给 CI。
五、调试方法与常见的坑
配了没反应,按顺序查:
- 配置改完不是即插即用,会话里输入
/hooks确认已加载、挂对了位置 - 用
python -m json.tool settings.json验证 JSON 语法,少个逗号就全盘失效 - 手动喂 JSON 测脚本:
echo '{"tool_input":{"command":"rm -rf /"}}' | python block_danger.py,看退出码和 stderr 是否符合预期 - Windows 的双重转义最容易翻车:JSON 里的引号要写成
",命令一复杂就老老实实写成脚本文件 - 路径含空格必须加引号;hook 变慢就检查是不是把重型命令挂在了 PostToolUse

还有两个容易忽略的细节。一是触发频率 :Stop、Notification 这类事件每一轮对话都会触发,UserPromptSubmit 每次提交都触发,挂上命令前先想想"它一天会被调用几百次吗",提醒类尤其要轻。二是怎么停用 :不想要了就把对应配置从 settings.json 里删掉,或者先挪到 settings.local.json 里观察几天;改完同样用 /hooks 复核,确认旧配置已经不在列表里。
六、安全提醒:这是把真钥匙
Hooks 以你的用户权限执行任意 shell 命令,等于交出账号级别的权限,所以:
- 抄配置前逐字读 command。来路不明、看不懂、带下载执行的,一律不抄
- 拦截类 hook 是兜底不是唯一防线,和权限规则配合用,别因为挂了 hook 就全程免确认
- 改完配置用
/hooks复核;脚本里不要写死任何密钥 - 从标准输入读到的 JSON 是输入不是信任,脚本内先校验再使用

结语
Hooks 的价值在于把"每次都要叮嘱"变成"每次自动发生":格式化交给 ①,底线交给 ②③,体验交给 ④,质量交给 ⑤。建议从 ④ 开始试水,确认机制跑通,再上会拦人的 ②③。