实战笔记:创建PostToolUse``PreToolUse
实战一:创建 PostToolUse Hook,实现 Go 代码自动格式化
目标是:
- 监听
Edit|Write|MultiEdit - 当工具执行成功
- 且目标文件是
.go - 自动执行:
gofmt -wgoimports -w
推荐做法:用脚本文件,而不是把长 Bash 一股脑塞进 settings.json
虽然 /hooks 菜单里可以直接填一长串命令,但对于团队项目,更推荐脚本化:
- 更容易读
- 更容易调试
- 更容易 review
- 后续扩展更方便
第一步:创建 Hook 脚本
在项目里新建文件:
mkdir -p .claude/hooks
touch .claude/hooks/format_go_post_tool_use.sh
chmod +x .claude/hooks/format_go_post_tool_use.sh
写入以下内容:
#!/usr/bin/env bash
set -eu
json="$(cat)"
success="$(printf '%s' "$json" | jq -r '.tool_response.success // false')"
file="$(printf '%s' "$json" | jq -r '.tool_input.file_path // empty')"
if [ "$success" != "true" ]; then
exit 0
fi
if [ -z "$file" ]; then
exit 0
fi
case "$file" in
*.go)
gofmt -w "$file"
goimports -w "$file"
printf '{"message":"Formatted Go file: %s"}\n' "$file"
;;
*)
exit 0
;;
esac
这个脚本在做什么
1. 读取 Hook 事件 JSON
Claude Code 会把事件数据通过 stdin 传给脚本:
json="$(cat)"
2. 判断这次工具调用是否成功
只在成功写入文件后才继续:
success="$(printf '%s' "$json" | jq -r '.tool_response.success // false')"
3. 取出被修改的文件路径
file="$(printf '%s' "$json" | jq -r '.tool_input.file_path // empty')"
4. 只处理 .go 文件
case "$file" in
*.go)
5. 自动格式化
gofmt -w "$file"
goimports -w "$file"
6. 返回一条结构化消息
方便 debug,也方便 Claude Code 记录:
printf '{"message":"Formatted Go file: %s"}\n' "$file"
第二步:在 settings.json 里注册 PostToolUse Hook
把下面内容加入 ./.claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format_go_post_tool_use.sh"
}
]
}
]
}
}
如果你原来 settings.json 已经有其他字段,比如 permissions 和 sandbox,那就把 hooks 合并进去,不要覆盖掉原有配置。
第三步:确保权限配置允许格式化命令执行
因为你前面已经建立了权限体系,所以还要确认:
"permissions": {
"allow": [
"Bash(gofmt:*)",
"Bash(goimports:*)"
]
}
否则 Hook 虽然匹配到了,但实际执行时会被权限系统拦住。
第四步:验证效果
你可以找一个 Go 文件,比如:
package main
import "fmt"
func main(){fmt.Println("hello")}
然后让 Claude 修改它,例如:
@main.go 在文件里增加一个空导入 time 包
如果 Hook 生效,那么 Claude 写完文件后,这个文件会自动变成规范格式,例如:
package main
import (
"fmt"
_ "time"
)
func main() {
fmt.Println("hello")
}
也就是说:
- AI 负责写
- Hook 自动兜底格式化
- 团队不用再反复提醒 gofmt/goimports
调试方式
如果没生效,建议用:
claude --debug
然后观察 debug 日志。
重点看这些信息:
Getting matching hook commands for PostToolUseMatched 1 unique hooksParsed initial responseFormatted Go file: ...
PostToolUse 方案一句话总结
这个 Hook 的本质是:
把“每次改完 Go 文件后手动 gofmt/goimports”这件重复劳动,变成事件驱动的自动化收尾动作。
实战二:创建 PreToolUse Hook,阻止对 main/master 分支的直接修改
这个 Hook 的目标更偏“安全治理”:
- 监听
Edit|Write|MultiEdit - 在真正执行之前检查当前分支
- 如果是
main或master - 直接阻止修改操作
第一步:创建分支保护脚本
在项目里创建:
touch .claude/hooks/check_main_branch.py
chmod +x .claude/hooks/check_main_branch.py
写入以下内容:
#!/usr/bin/env python3
import json
import subprocess
import sys
def get_current_branch() -> str:
try:
result = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
capture_output=True,
text=True,
check=True,
timeout=5,
)
return result.stdout.strip()
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
return ""
def is_protected_branch(branch: str) -> bool:
return branch.lower() in ("main", "master")
def main() -> None:
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError as err:
print(f"invalid hook input json: {err}", file=sys.stderr)
sys.exit(1)
tool_name = input_data.get("tool_name", "")
if tool_name not in ("Edit", "Write", "MultiEdit"):
sys.exit(0)
branch = get_current_branch()
if not branch:
sys.exit(0)
if is_protected_branch(branch):
print(
f"🚫 Cannot modify files on protected branch '{branch}'.\n"
f"Please create or switch to a feature branch before editing.",
file=sys.stderr,
)
sys.exit(2)
sys.exit(0)
if __name__ == "__main__":
main()
这个脚本在做什么
1. 从 stdin 读取 Hook 事件 JSON
虽然当前逻辑主要用的是 Git 状态,但还是规范地读取了输入。
2. 判断当前工具是否为写类工具
只拦:
EditWriteMultiEdit
3. 获取当前 Git 分支
git rev-parse --abbrev-ref HEAD
4. 如果当前分支是 main/master,退出码为 2
这一步最关键:
sys.exit(2)
exit code 2 会阻止工具执行,并把 stderr 的内容反馈给 Claude。
第二步:注册 PreToolUse Hook
把下面内容加入 ./.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check_main_branch.py"
}
]
}
]
}
}
如果你已经有 PostToolUse Hook,最终 hooks 可以写成这样
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check_main_branch.py"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format_go_post_tool_use.sh"
}
]
}
]
}
}
第三步:验证效果
先切到主分支:
git checkout main
然后让 Claude 修改任意文件,比如:
@internal/converter/converter.go 在文件末尾增加一个空函数
如果 Hook 生效,Claude 不会真的执行写操作,而是会直接收到类似提示:
🚫 Cannot modify files on protected branch 'main'.
Please create or switch to a feature branch before editing.
这就说明:
PreToolUse生效了- 工具调用被拦下来了
- Claude 还能根据错误信息调整后续行为
最终推荐:issue2md 项目的 hooks 组合
对于 Go 项目,我非常建议同时启用这两个 Hook:
1. PreToolUse
负责 保护主分支
作用:
- 防止误改
main/master - 强制在 feature branch 上工作
- 提高仓库安全性
2. PostToolUse
负责 自动格式化 Go 代码
作用:
- 自动
gofmt - 自动
goimports - 保证代码风格一致
- 降低人工清理成本
推荐的最终目录结构
issue2md/
├─ .claude/
│ ├─ settings.json
│ └─ hooks/
│ ├─ format_go_post_tool_use.sh
│ └─ check_main_branch.py
推荐的最终 settings.json 中 hooks 片段
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check_main_branch.py"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format_go_post_tool_use.sh"
}
]
}
]
}
}
一句话总结
这两个 Hook 一前一后,分别解决了两件事:
- PreToolUse:不让 AI 在危险上下文里乱动手
- PostToolUse:让 AI 写完 Go 代码后自动完成规范化收尾
也就是说,你不是只是在“用 AI 改代码”,而是在:
给 AI 建立一个事件驱动的工程执行护栏 + 自动化收尾系统。