Claude Code 实战入门(五):Hooks,让你不再担心 Yolo 模式

Hooks 是每次工具调用前后跑的 shell 脚本。PreToolUse 可以阻止。PostToolUse 可以格式化、Lint、记日志。我每个 Repo 都用的 5 个 Hook,加上一个把所有人都坑过的反模式。

如果 MCP 是 Claude 伸出去的手,Hooks 是你伸进来的手。它是把你在意的规则从"希望"变成"强制"的方式。

模型

Hook 是 shell 命令。Claude Code 在几个良好定义的时刻跑它。常用两个:

  • PreToolUse——在工具调用之前。退出码 0 放行;非 0 阻止。
  • PostToolUse——在工具返回之后。退出码仅作信息;可用于格式化文件、跑 Lint、记日志。

还有别的(UserPromptSubmitStopNotificationSubagentStop)。日常这两个是 90% 的价值。

Hook 住哪

.claude/settings.json(或 .local 变体)里。最小例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "/path/to/check-bash.sh" }
        ]
      }
    ]
  }
}

matcher 决定 Hook 应用到哪些工具。"Bash" 匹内置 Bash。"Write" 匹文件写入。"mcp__playwright__.*" 匹所有 Playwright MCP 工具。支持正则——通配很常见。

Hook 命令通过 stdin 拿到工具输入的 JSON,对话上下文以环境变量给出。最简 Hook 就是读 stdin、判断、用合适退出码退出。

Hook 1:禁危险命令

.claude/hooks/check-bash.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/usr/bin/env bash
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command')

# 任何广义破坏性的,拒
if echo "$cmd" | grep -E '(rm -rf /|sudo rm|chmod -R 777)' >/dev/null; then
  echo "Refusing —— 检测到破坏性模式:$cmd" >&2
  exit 1
fi
exit 0

PreToolUse matcher Bash。阻止以 1 退出;stderr 上的消息回到模型,让它知道为什么并能调整。这就是"Agent 静默崩溃"和"Agent 学会要权限"的差别。

Hook 2:写入时自动格式化

.claude/hooks/format-on-write.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/usr/bin/env bash
input=$(cat)
path=$(echo "$input" | jq -r '.tool_input.file_path')

case "$path" in
  *.py) ruff format "$path" 2>/dev/null ;;
  *.ts|*.tsx|*.js|*.jsx) prettier --write "$path" 2>/dev/null ;;
  *.go) gofmt -w "$path" 2>/dev/null ;;
esac
exit 0

PostToolUse matcher Write|Edit。Claude 每次动文件,下一轮之前文件已被格式化。Agent 永远不需要被告知格式——你的房屋风格被代码强制。

Hook 3:记每一次工具调用

1
2
3
4
5
#!/usr/bin/env bash
input=$(cat)
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $(jq -c . <<< "$input")" \
  >> "$CLAUDE_PROJECT_DIR/.claude/tool-log.jsonl"
exit 0

PostToolUse matcher .*。每次工具调用一行 JSON 写到文件。出问题——或没出问题——的时候你有完整审计轨迹。

我精确用过它三次。三次都是事后复盘,需要知道"那 40 分钟会话里 Claude 到底干了什么"。值它占的硬盘。

Hook 4:commit 前强制测试通过

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/usr/bin/env bash
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command')
if echo "$cmd" | grep -qE '^git commit'; then
  if ! npm test --silent >/dev/null 2>&1; then
    echo "拒绝 commit:测试失败。跑 'npm test' 看详情。" >&2
    exit 1
  fi
fi
exit 0

PreToolUse matcher Bash。拦 git commit,先跑测试。挂了就拦下来,告诉模型为什么。给 Agent 自己的 pre-commit hook。

听起来挺霸道,是的。重点是你不能意外提交破坏代码。模型必须主动绕开 Hook 才能做错事,而它不会。

Hook 5:擦工具输出里的密钥

1
2
3
4
5
#!/usr/bin/env bash
sed -E '
  s/(Bearer|sk-)[A-Za-z0-9_-]{20,}/\1[REDACTED]/g
  s/(api[_-]?key["\s:=]+)["A-Za-z0-9_-]{16,}/\1[REDACTED]/gI
'

PostToolUse matcher Read|Bash。在 Agent 看到工具输出之前过一遍流式擦除器。如果你不小心 cat 了带密钥的文件,模型永远读不到——它们就地变成 [REDACTED]

这是处理真实代码库时单条最重要的安全 Hook。

反模式:相对路径

最常见的 Hook Bug 是命令用相对路径:

1
{ "command": "./scripts/check.sh" }   // 错

Claude Code 在你不控制的工作目录跑 Hook。永远用绝对路径或 $CLAUDE_PROJECT_DIR

1
{ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/check.sh" }  // 对

脚本内部同理:别 cd、别用相对路径。

Hook 在团队工作流里的位置

提交进 .claude/settings.json 的 Hook 对所有人生效。这就是重点。新队友克隆 Repo、跑 claude,自动继承你的安全栏和格式化策略。零配置、零选择加入。

仅个人用的 Hook(依赖你独有工具的)放 .claude/settings.local.json。它们留在你机器上。

Hook 不是什么

  • 不是权限的替代。 Hook 可以补充权限,但不能替代。权限是声明式、显式;Hook 是可执行、易写错。
  • 不是免费检查。 每个 Hook 给每次工具调用加延迟。5 个 Hook 各 100ms 就是每工具 0.5 秒。盯预算。
  • 不是图灵完备配置。 当 Hook 开始像小程序时,你应该写一个 MCP 服务器。

下一篇是 SDK 与 GitHub 集成——程序化的 Claude Code,跑在 CI 里、对 PR 工作。系列结尾,也是最强的一篇。

翻完了?

去 GitHub 关注一下,新一篇通常隔一周就到。

GitHub