【AI生成】学霸笔记:11|实战笔记:创建`PostToolUse``PreToolUse`

蛋蛋 2026年03月29日 1 0

实战笔记:创建PostToolUse``PreToolUse

实战一:创建 PostToolUse Hook,实现 Go 代码自动格式化

目标是:

  • 监听 Edit|Write|MultiEdit
  • 当工具执行成功
  • 且目标文件是 .go
  • 自动执行:
    • gofmt -w
    • goimports -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 已经有其他字段,比如 permissionssandbox,那就把 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 PostToolUse
  • Matched 1 unique hooks
  • Parsed initial response
  • Formatted Go file: ...

PostToolUse 方案一句话总结

这个 Hook 的本质是:

把“每次改完 Go 文件后手动 gofmt/goimports”这件重复劳动,变成事件驱动的自动化收尾动作。


实战二:创建 PreToolUse Hook,阻止对 main/master 分支的直接修改

这个 Hook 的目标更偏“安全治理”:

  • 监听 Edit|Write|MultiEdit
  • 在真正执行之前检查当前分支
  • 如果是 mainmaster
  • 直接阻止修改操作

第一步:创建分支保护脚本

在项目里创建:

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. 判断当前工具是否为写类工具

只拦:

  • Edit
  • Write
  • MultiEdit

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 建立一个事件驱动的工程执行护栏 + 自动化收尾系统。

Last Updated: 2026/03/29 16:03:05
【AI生成】学霸笔记:12||终极扩展:深入 MCP 服务器,将 AI 连接到任何内外部系统 【AI生成】学霸笔记:11|事件驱动:详解 Hooks 机制,让 AI 在关键节点自动触发