用 Claude Code 写代码的人越来越多了,但有个问题一直在:Claude 改完文件,你得手动跑 prettier;Claude 跑了个 rm 命令,你心里一紧;Claude 干完活了,你还得自己切到终端检查结果。

这些重复操作,Hooks 可以全部自动化。

我在日常开发中配了 5 个 Hooks,跑了两个月,这篇文章把完整配置和踩坑经验写出来。你看完直接复制到自己项目里就能用。

Hooks 是什么

Hooks 是 Claude Code 的生命周期钩子。你定义一段 shell 命令,绑定到某个事件上,Claude 执行到那个节点时自动触发。

比如 Claude 要执行一个 shell 命令(PreToolUse 事件),你的 hook 先跑一遍,检查命令里有没有 rm -rf。有的话直接拦截,Claude 收到一条"被阻止"的消息,不会真的执行。

整个过程不需要你盯着屏幕,不需要你按确认键。

Hooks 支持 20 多种事件,常用的就这几个:

  • PreToolUse:工具调用前触发,可以拦截
  • PostToolUse:工具调用后触发,可以做格式化、校验
  • Stop:Claude 回复完毕时触发,可以做收尾工作
  • SessionStart:会话开始时触发,可以做环境初始化
  • Notification:Claude 发通知时触发,可以转发到飞书/Slack

配置文件放哪

Hooks 配置写在 JSON 文件里,放在不同位置作用域不同:

~/.claude/settings.json           全局生效,所有项目
.claude/settings.json             当前项目,可以提交到 Git
.claude/settings.local.json       当前项目,不入库

团队协作推荐用 .claude/settings.json,个人习惯放全局。

实战配置 1:拦截危险命令

这个最刚需。Claude 有时候会执行 rm -rf 或者 git push --force 这种不可逆操作。配一个 PreToolUse 钩子拦住它。

.claude/settings.json 里加:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/block-dangerous.sh"
          }
        ]
      }
    ]
  }
}

钩子脚本 .claude/hooks/block-dangerous.sh

#!/bin/bash
COMMAND=$(cat | jq -r '.tool_input.command')

# 危险命令黑名单
BLOCKED_PATTERNS=(
  "rm -rf /"
  "rm -rf ~"
  "git push --force"
  "git push -f"
  "DROP TABLE"
  "DROP DATABASE"
)

for pattern in "${BLOCKED_PATTERNS[@]}"; do
  if echo "$COMMAND" | grep -qi "$pattern"; then
    jq -n '{
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "deny",
        permissionDecisionReason: "危险命令被拦截: '"$pattern"'"
      }
    }'
    exit 0
  fi
done

exit 0

记得加执行权限:chmod +x .claude/hooks/block-dangerous.sh

踩坑提醒:exit code 很讲究。PreToolUse 里 exit 0 表示放行,返回 JSON 里带 permissionDecision: "deny" 表示拦截。别搞混了。exit code 2 也可以直接拦截,但不能附带原因说明,不推荐。

实战配置 2:写完文件自动格式化

Claude 改完代码经常不符合项目的格式规范。配一个 PostToolUse 钩子,每次写文件后自动跑 prettier。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/auto-format.sh"
          }
        ]
      }
    ]
  }
}

脚本 .claude/hooks/auto-format.sh

#!/bin/bash
FILE_PATH=$(cat | jq -r '.tool_input.file_path // .tool_input.filePath // empty')

if [ -z "$FILE_PATH" ]; then
  exit 0
fi

# 只格式化前端相关文件
case "$FILE_PATH" in
  *.js|*.ts|*.jsx|*.tsx|*.css|*.json|*.md)
    npx prettier --write "$FILE_PATH" 2>/dev/null
    ;;
  *.py)
    black "$FILE_PATH" 2>/dev/null
    ;;
esac

exit 0

实测数据:我的项目里 Claude 一天平均改 30-50 个文件。没配这个钩子前,每天手动跑 prettier 大概花 10-15 分钟。配了之后完全省掉了。

踩坑提醒:PostToolUse 钩子不要做太慢的事。它是同步执行的,跑太久会拖慢 Claude 的响应。prettier 格式化一个文件通常 200ms 以内,没问题。但如果你想在这里跑完整的 eslint fix,可能需要改成异步钩子(加 "async": true)。

实战配置 3:完成任务后自动跑测试

Claude 写完代码说"搞定了",但测试有没有过?配一个 Stop 钩子,Claude 每次回复完自动跑测试。

{
  "hooks": {
    "Stop": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/run-tests.sh",
            "async": true
          }
        ]
      }
    ]
  }
}

脚本 .claude/hooks/run-tests.sh

#!/bin/bash
cd "$CLAUDE_PROJECT_DIR" || exit 0

# 检查是否有改动的测试文件
CHANGED_FILES=$(git diff --name-only HEAD 2>/dev/null)

if echo "$CHANGED_FILES" | grep -qE '\.(test|spec)\.(js|ts|jsx|tsx)$'; then
  # 只跑改动相关的测试
  npm test -- --changedSince=HEAD~1 2>&1 | tail -20 > /tmp/claude-test-result.txt

  if [ $? -ne 0 ]; then
    # 测试失败,发通知
    osascript -e 'display notification "有测试用例失败了" with title "Claude Code"' 2>/dev/null
  fi
fi

exit 0

这里用了 "async": true,测试在后台跑,不阻塞 Claude 的下一轮对话。测试结果写到 /tmp/claude-test-result.txt,随时可以查。

踩坑提醒:Stop 事件每次 Claude 回复完都会触发,包括它只是回答一个问题、没改任何代码的时候。所以脚本里要先判断有没有文件改动,不要无脑跑测试。

实战配置 4:会话开始时检查环境

有时候切换项目忘了启动 Docker、忘了切 Node 版本。SessionStart 钩子可以在每次开始对话时做环境检查。

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/check-env.sh"
          }
        ]
      }
    ]
  }
}

脚本 .claude/hooks/check-env.sh

#!/bin/bash
WARNINGS=""

# 检查 Node 版本
REQUIRED_NODE="20"
CURRENT_NODE=$(node -v 2>/dev/null | grep -oE '[0-9]+' | head -1)
if [ "$CURRENT_NODE" != "$REQUIRED_NODE" ]; then
  WARNINGS="${WARNINGS}Node 版本不对,当前 v${CURRENT_NODE},需要 v${REQUIRED_NODE}\n"
fi

# 检查 Docker
if ! docker info > /dev/null 2>&1; then
  WARNINGS="${WARNINGS}Docker 没启动\n"
fi

# 检查 .env 文件
if [ ! -f "$CLAUDE_PROJECT_DIR/.env" ]; then
  WARNINGS="${WARNINGS}.env 文件不存在\n"
fi

if [ -n "$WARNINGS" ]; then
  echo "⚠️ 环境检查发现问题:"
  echo -e "$WARNINGS"
fi

exit 0

钩子的 stdout 输出会被 Claude 读到。所以环境检查的警告信息 Claude 能看见,它会在后续操作中考虑这些情况。

实战配置 5:任务完成发通知到飞书

Claude 跑一个耗时任务,你不想干等着。配一个 Notification 钩子把消息转发到飞书群。

{
  "hooks": {
    "Notification": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/notify-feishu.sh",
            "async": true
          }
        ]
      }
    ]
  }
}

脚本 .claude/hooks/notify-feishu.sh

#!/bin/bash
INPUT=$(cat)
MESSAGE=$(echo "$INPUT" | jq -r '.message // "Claude Code 发来通知"')

# 飞书群机器人 webhook
WEBHOOK_URL="https://open.feishu.cn/open-apis/bot/v2/hook/你的webhook地址"

curl -s -X POST "$WEBHOOK_URL" \
  -H "Content-Type: application/json" \
  -d "{
    \"msg_type\": \"text\",
    \"content\": {
      \"text\": \"[Claude Code] $MESSAGE\"
    }
  }" > /dev/null

exit 0

这样 Claude 干完活或者遇到问题需要你介入时,飞书群里会收到消息。比轮询终端窗口舒服多了。

完整的 settings.json 参考

把上面 5 个配置合在一起:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "*",
        "hooks": [
          { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/check-env.sh" }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/block-dangerous.sh" }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/auto-format.sh" }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "*",
        "hooks": [
          { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/run-tests.sh", "async": true }
        ]
      }
    ],
    "Notification": [
      {
        "matcher": "*",
        "hooks": [
          { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/notify-feishu.sh", "async": true }
        ]
      }
    ]
  }
}

调试技巧

钩子不生效?几个排查方向:

  1. 脚本有没有执行权限chmod +x .claude/hooks/*.sh 先跑一遍
  2. jq 有没有装:钩子的输入是 JSON,解析靠 jq。brew install jqapt install jq
  3. 手动测试脚本echo '{"tool_input":{"command":"rm -rf /"}}' | bash .claude/hooks/block-dangerous.sh 看输出对不对
  4. 看 Claude Code 日志:运行 claude --debug 可以看到钩子的触发和输出

环境变量 $CLAUDE_PROJECT_DIR 是 Claude Code 自动注入的,指向当前项目根目录。如果你的脚本在非项目目录下测试,这个变量是空的,要注意。

写在最后

Hooks 的设计思路很简单:Claude 是非确定性的(LLM 嘛),但你的工程流程需要确定性。Hooks 在两者之间加了一层确定性的控制点。

这 5 个配置覆盖了我日常 80% 的场景。你可以根据自己的项目需求改脚本内容,整体框架直接复制就行。

如果你想了解更多事件类型(比如 SubagentStart、FileChanged、WorktreeCreate),可以查看官方文档 code.claude.com/docs/en/hooks。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐