1. 引言

在这里插入图片描述

在前面的博客,博主已经讲解了 Claude Code 相关的概念,有兴趣的同学可以参考下:

如果你已经在用 Claude Code 写代码,你一定遇到过这些“重复但必要”的动作:

  • 写完文件要跑格式化、lint、单测
  • 运行危险 Bash 之前想要一层保险(尤其是 rm -rfgit push --force
  • 想在会话开始时自动检查环境(Node/Python 版本、依赖是否安装)
  • 想在 Claude “准备停下”时强制做一次自检:测试过了吗?有未提交的改动吗?

Hooks 的价值就在于把这些动作从“ 你记得做” 变成 “系统自动做” ,并且可以做到“该阻断就阻断、该后台跑就后台跑、该提醒就提醒”

2. Hooks 是什么?

Hooks 是 Claude Code 的事件驱动自动化系统:在特定事件触发时,自动执行一组 Hook。

它主要有两种形态:

  1. 命令型 Hooks(command):运行 Shell 命令/脚本,适合自动化与校验(格式化、检查、通知、跑测试……)。
  2. 提示型 Hooks(prompt):在 Stop / SubagentStop 事件里,让 Claude 再做一次“停下前的质量检查”,根据结果决定继续还是停止。

你可以把它理解为:

  • SessionStart/End:会话生命周期钩子
  • PreToolUse/PostToolUse:工具执行前/后钩子(Write/Edit/Bash/Read…)
  • Stop/SubagentStop:模型准备结束前的“收尾门禁”
  • Notification:系统通知的自定义处理

3. Hooks 配置(用户级 vs 项目级)

Claude Code 支持分层配置,Hooks 通常按是否团队共享来决定目录。

项目级(推荐团队共享):

  • 文件:{project}/.claude/settings.json
  • 适合:团队统一的格式化、测试、危险命令拦截、质量门禁等
  • 配套建议:把脚本也放进仓库,例如 {project}/.claude/hooks/*.sh,让团队开箱即用

项目级本地(仅自己用,不提交):

  • 文件:{project}/.claude/settings.local.json
  • 适合:个人路径相关脚本、个人通知方式、私有工具等
  • 配套建议:加入 .gitignore

用户级(跨项目生效)

  • 文件:~/.claude/settings.json
  • 适合:你个人的通用 Hooks(比如任何项目都启用的通知/危险命令拦截)

小贴士:团队规范建议 “项目级为主、用户级为辅”,项目级让仓库可复制;用户级放个人偏好。

4. 八种事件类型汇总

事件 触发时机 Hook 类型 常见用途
SessionStart 会话开始 命令型 初始化环境、检查依赖、打印提示
SessionEnd 会话结束 命令型 清理临时文件、记录统计
UserPromptSubmit 用户提交输入前 命令型 输入验证、关键词拦截、注入上下文
PreToolUse 工具执行前 命令型 危险命令拦截、参数校验、审计
PostToolUse 工具执行后 命令型 自动格式化、自动测试、生成变更摘要
Stop Claude 决定停止前 提示型 质量门禁(lint/test/git status)
SubagentStop 子代理停止前 提示型 子任务验收(比如“测试是否补齐”)
Notification Claude 发送通知时 命令型 转发通知(系统通知/Slack/飞书等)

5. 命令型 Hooks

写脚本就能自动化

5.1 基础语法(最常用)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "./.claude/hooks/format.sh",
            "blocking": false
          }
        ]
      }
    ]
  }
}

关键点:

  • matcher:用于筛选工具(通常写正则/或模式串),比如 Write|EditBash
  • blocking
    • true:Hook 失败会阻断当前动作(适合安全校验、门禁)
    • false:Hook 失败不阻断(适合格式化、统计、通知)

5.2 Hook 能拿到什么上下文?

命令型 Hooks 通常可以通过环境变量拿到上下文(不同事件变量略有差异,常见如):

变量名 说明
TOOL_NAME 工具名称(如 Write/Edit/Bash
TOOL_INPUT 工具输入(JSON 字符串)
TOOL_INPUT_FILE_PATH 文件路径(Write/Edit 常用)
TOOL_OUTPUT 工具输出(PostToolUse 常用)
USER_MESSAGE 用户输入(UserPromptSubmit)
CLAUDE_WORKING_DIR 当前工作目录
NOTIFICATION_MESSAGE 通知内容(Notification,JSON)

更稳妥的写法:脚本里同时支持读取 stdin(JSON)与环境变量,避免某些环境差异导致取不到值。

5.3 退出码怎么控制“阻断/不阻断”?

常见约定(建议你按这个写,团队最好统一):

exit 0   # 成功,继续
exit 2   # 阻断错误(blocking=true 时会拦下本次操作)
exit 1   # 非阻断错误(记录/提示,但不中断主流程)

6. 提示型 Hooks

让 Claude 在“停下前”做质量门禁

提示型 Hooks 仅用于 Stop / SubagentStop:当 Claude 觉得“差不多可以结束了”,会先跑一次这个提示,让它自检并输出 JSON 决策。

6.1 基础语法

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "检查是否满足停止条件。返回 JSON: {\"decision\":\"stop\"|\"continue\",\"feedback\":\"说明\"}"
          }
        ]
      }
    ]
  }
}

5.2 响应格式(核心)

{
  "decision": "continue",
  "feedback": "请先运行测试并修复 lint。"
}

你可以用它来实现 “Claude 自己给自己验收” 的体验,不满足条件就继续把事情做完,满足才停。

7. 案例模板

下面的模板尽量遵循一个原则:高风险校验 blocking=true,耗时工作 blocking=false

依赖提示:后面的脚本示例会用到 jq 来解析 JSON(macOS 可用 brew install jq)。

7.1 案例:SessionStart 做环境体检(不阻断)

session-init.sh:检查 Node/Python/Git 状态、依赖是否安装等(按你的项目改)。

.claude/settings.json配置如下:

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          { "type": "command", "command": "./.claude/hooks/session-init.sh", "blocking": false }
        ]
      }
    ]
  }
}

session-init.sh 示例代码如下:

#!/usr/bin/env bash
set -euo pipefail

echo "🚀 Claude Code SessionStart"
echo "cwd: ${CLAUDE_WORKING_DIR:-$(pwd)}"

if command -v git >/dev/null 2>&1 && git rev-parse --git-dir >/dev/null 2>&1; then
  echo "git branch: $(git branch --show-current)"
  changes="$(git status --porcelain | wc -l | tr -d ' ')"
  if [[ "${changes}" != "0" ]]; then
    echo "⚠️  uncommitted changes: ${changes}"
  fi
fi

if [[ -f package.json ]]; then
  command -v node >/dev/null 2>&1 && echo "node: $(node --version)"
  [[ -d node_modules ]] || echo "📦 tip: run npm install"
fi

if [[ -f requirements.txt ]]; then
  command -v python >/dev/null 2>&1 && echo "python: $(python --version 2>&1 | head -n 1)"
  [[ -n "${VIRTUAL_ENV:-}" ]] || echo "🐍 tip: activate venv"
fi

7.2 案例:PreToolUse 拦截危险 Bash(阻断)

guard-bash.sh 思路:从 TOOL_INPUT(或 stdin JSON)里取出 command,匹配 rm -rfgit push --force 等高危模式,命中就 exit 2 并在 stderr 输出原因。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "./.claude/hooks/guard-bash.sh", "blocking": true }
        ]
      }
    ]
  }
}

guard-bash.sh 示例代码如下:

#!/usr/bin/env bash
set -euo pipefail

input_json="${TOOL_INPUT:-}"
if [[ -z "${input_json}" ]]; then
  input_json="$(cat || true)"
fi

cmd="$(echo "${input_json}" | jq -r '.command // empty' 2>/dev/null || true)"
if [[ -z "${cmd}" ]]; then
  exit 0
fi

if [[ "${cmd}" == *"[claude-allow-danger]"* ]]; then
  exit 0
fi

deny_patterns=(
  '(^|[[:space:]])rm[[:space:]]+-rf[[:space:]]+(/|~|\\$HOME)([[:space:]]|$)'
  '(^|[[:space:]])git[[:space:]]+push([[:space:]].*)?--force'
  '(^|[[:space:]])git[[:space:]]+reset([[:space:]].*)?--hard'
  '(^|[[:space:]])chmod[[:space:]]+-R[[:space:]]+777'
)

for pattern in "${deny_patterns[@]}"; do
  if echo "${cmd}" | grep -Eq "${pattern}"; then
    echo "❌ 已阻断高危命令(PreToolUse):" >&2
    echo "    ${cmd}" >&2
    echo "如确认要执行,请在命令末尾加 [claude-allow-danger] 作为显式确认。" >&2
    exit 2
  fi
done

exit 0

7.3 案例:PreToolUse 做敏感信息检测(阻断)

适用场景:Write/Edit 写入内容前,扫描是否疑似 Key/密码/Token,命中直接拦截。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          { "type": "command", "command": "./.claude/hooks/check-secrets.sh", "blocking": true }
        ]
      }
    ]
  }
}

check-secrets.sh示例代码如下:

#!/usr/bin/env bash
set -euo pipefail

input_json="${TOOL_INPUT:-}"
if [[ -z "${input_json}" ]]; then
  input_json="$(cat || true)"
fi
content="$(echo "${input_json}" | jq -r '.new_string // .content // ""' 2>/dev/null || true)"

patterns=(
  'AKIA[0-9A-Z]{16}'                  # AWS Access Key
  'ghp_[a-zA-Z0-9]{36}'               # GitHub PAT
  'sk-[a-zA-Z0-9]{32,}'               # API Key(示例)
  '(?i)password\\s*[:=]\\s*["\x27][^"\x27]+["\x27]'
  '(?i)api[_-]?key\\s*[:=]\\s*["\x27][^"\x27]+["\x27]'
)

for pattern in "${patterns[@]}"; do
  if echo "${content}" | grep -Pq "${pattern}" 2>/dev/null; then
    echo "❌ 检测到潜在敏感信息,已阻断写入(PreToolUse)" >&2
    echo "pattern: ${pattern}" >&2
    echo "建议:改用环境变量/密钥管理服务,不要把 Key 写进仓库。" >&2
    exit 2
  fi
done

exit 0

7.4 案例:PostToolUse 自动格式化 + 自动跑测试(不阻断)

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

format.sh 示例代码如下:

#!/usr/bin/env bash
set -euo pipefail

file="${TOOL_INPUT_FILE_PATH:-}"
[[ -n "${file}" && -f "${file}" ]] || exit 0

case "${file}" in
  *.ts|*.tsx|*.js|*.jsx|*.json)
    command -v prettier >/dev/null 2>&1 && prettier --write "${file}" >/dev/null 2>&1 || true
    command -v eslint >/dev/null 2>&1 && eslint --fix "${file}" >/dev/null 2>&1 || true
    ;;
  *.py)
    command -v black >/dev/null 2>&1 && black "${file}" >/dev/null 2>&1 || true
    command -v isort >/dev/null 2>&1 && isort "${file}" >/dev/null 2>&1 || true
    ;;
  *.go)
    command -v gofmt >/dev/null 2>&1 && gofmt -w "${file}" >/dev/null 2>&1 || true
    ;;
  *.rs)
    command -v rustfmt >/dev/null 2>&1 && rustfmt "${file}" >/dev/null 2>&1 || true
    ;;
esac

exit 0

auto-test.sh 示例代码如下:

#!/usr/bin/env bash
set -euo pipefail

file="${TOOL_INPUT_FILE_PATH:-}"
[[ -n "${file}" && -f "${file}" ]] || exit 0

if echo "${file}" | grep -Eq '(\\.test\\.|\\.spec\\.|/tests?/|__tests__)'; then
  exit 0
fi

if [[ -f package.json ]] && command -v npm >/dev/null 2>&1; then
  if echo "${file}" | grep -Eq '\\.(ts|tsx|js|jsx)$'; then
    echo "🧪 running minimal js/ts tests (best effort)"
    npm test --silent 2>/dev/null || true
  fi
fi

if [[ -f pyproject.toml || -f requirements.txt ]] && command -v pytest >/dev/null 2>&1; then
  if echo "${file}" | grep -Eq '\\.py$'; then
    echo "🧪 running minimal python tests (best effort)"
    pytest -q 2>/dev/null || true
  fi
fi

exit 0

建议:

  • 格式化脚本按后缀选择 prettier/black/gofmt/rustfmt
  • 测试脚本“只跑与当前文件相关的最小集合”,避免每次都全量测试拖慢体验

7.5 案例:Notification 做系统通知(不阻断)

notify.sh(macOS)可以用 osascriptterminal-notifier,从 NOTIFICATION_MESSAGE 里取 title/message 发系统通知。

{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          { "type": "command", "command": "./.claude/hooks/notify.sh", "blocking": false }
        ]
      }
    ]
  }
}

notify.sh 示例脚本如下:

#!/usr/bin/env bash
set -euo pipefail

msg_json="${NOTIFICATION_MESSAGE:-}"
if [[ -z "${msg_json}" ]]; then
  msg_json="$(cat || true)"
fi

title="$(echo "${msg_json}" | jq -r '.title // "Claude Code"' 2>/dev/null || echo "Claude Code")"
message="$(echo "${msg_json}" | jq -r '.message // empty' 2>/dev/null || true)"
[[ -n "${message}" ]] || exit 0

if command -v terminal-notifier >/dev/null 2>&1; then
  terminal-notifier -title "${title}" -message "${message}" -group "claude-code" >/dev/null 2>&1 || true
  exit 0
fi

osascript -e "display notification \"${message}\" with title \"${title}\"" >/dev/null 2>&1 || true
exit 0

8. 常见问题

问题一:Hook 不执行

可以通过如下方式排查:

开调试

claude --debug="hooks"

检查脚本可执行权限

chmod +x ./.claude/hooks/*.sh

脚本自测(用 stdin 模拟 JSON)

echo '{"sessionId":"test"}' | ./.claude/hooks/session-init.sh

问题二:无故被阻断

blocking=true 的 Hook 一定要做到:

  • 只在命中明确条件时才 exit 2
  • stderr 输出清晰原因(让 Claude 能把“为什么被拦”解释给你)

问题 三:变慢很明显

  • 把耗时任务(全量测试、扫描整个仓库)改成 blocking=false 或改成“增量/抽样”
  • 在脚本里加超时/短路条件(比如只对特定目录、特定后缀执行)

9. 文末

通过阅读本文,相信大家已经系统理解了 Claude Code 中 Hooks 的整体设计与实战价值:它并不是零散的脚本技巧,而是一套事件驱动的自动化与质量门禁机制。通过在 Session、Tool 执行前后以及 Stop 阶段合理配置命令型与提示型 Hooks,可以将格式化、测试、危险命令拦截、敏感信息校验等“重复但关键”的操作交由系统自动完成,把人为自觉升级为流程约束

希望本文能对大家深入理解和落地使用 Claude Code Hooks 有所帮助,也欢迎在评论区分享你的实战配置、踩坑经验与优化思路。感谢阅读,本文完!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐