Agent工坊

【Agent工坊】Claude Code Hooks实战:3个钩子让你的AI编程助手安全10倍

与其让模型记住你的项目规则,不如把规则写进 Hook 脚本——Hook 是 Claude Code 最被低估的能力,它让你从"祈祷模型听话"升级为"用代码强制执行策略"。

▲ 图1:Claude Code Hook 事件系统架构 — 27个事件、stdin/stdout ▲ ▲ 图1:Claude Code Hook 事件系统架构 — 27个事件、stdin/stdout JSON协议、5大核心Hook的决策流程

为什么你需要关心 Hook

用 Claude Code(或任何 AI Coding Agent)写过代码的人都遇到过类似困境:

  • 你告诉它"不要提交密钥到 Git",但它偶尔还是会漏掉
  • 你让它"改完代码跑测试",但它有时会跳过这一步
  • 你写了长长的 CLAUDE.md 项目规则,但长上下文下模型还是会忘

根本原因:把规则放在模型的记忆里,和把规则放在模型外面,是完全不同的两件事。 记忆会丢失、会忽略、会被后面的上下文覆盖。而 Hook 是模型每次调用工具时都要经过的一道门——不管你上下文有多长,不管你之前说了什么,Hook 脚本都会执行。

Claude Code 提供了 27 个 Hook 事件。本文基于对全部 Hook 的逐一研究,提炼出最值得投入的 5 个核心 Hook 和几个场景化 Hook,附带可直接使用的代码示例和踩坑经验。

Hook 的工作原理

.claude/settings.json 中配置 Hook:

{

  "hooks": {

    "PreToolUse": [

      {

        "matcher": "Bash",

        "hooks": [

          {

            "type": "command",

            "command": "/home/user/.claude/hooks/pre-bash.sh",

            "timeout": 30

          }

        ]

      }

    ]

  }

}

当事件触发时,Claude Code 执行你的命令,通过 stdin 传入 JSON,从 stdout 读取 JSON 决定下一步行为:

// stdin 示例(PreToolUse 事件)

{

  "event": "PreToolUse",

  "session_id": "abc123",

  "transcript_path": "/tmp/claude-transcript.json",

  "cwd": "/home/user/project",

  "tool_name": "Bash",

  "tool_input": { "command": "git push origin main" }

}

你的脚本通过 stdout 返回的控制选项:

  • continue: false — 直接中止本次调用
  • decision: "block" — 阻止工具执行,给出原因
  • hookSpecificOutput.additionalContext — 注入文本到下一个模型推理轮次
  • hookSpecificOutput.updatedInput — 改写工具的输入参数(PreToolUse 专用)
  • exit 0 + 空 stdout — 无操作,放行

非零退出码会作为 Hook 错误记录在对话记录中。

5 个核心 Hook,每个都有明确的适用场景

1. PreToolUse — 最强大的 Hook

触发时机:每次工具调用之前。

这是最重要的 Hook,因为它不仅能观察,还能改写输入updatedInput)。这意味着你可以在命令发给 Shell 执行之前就拦截并修改它。

实战场景一:防止密钥泄露

#!/bin/bash

# pre-bash.sh — 扫描命令中的密钥并拦截

INPUT=$(cat)

COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')

# 检测常见的密钥模式

if echo "$COMMAND" | grep -qE '(sk-[a-zA-Z0-9]{20,})|(ghp_[a-zA-Z0-9]{36})|(-----BEGIN.*PRIVATE KEY-----)'; then

  echo '{"continue": true, "decision": "block", "reason": "命令中包含疑似密钥/Token,已拦截。请使用环境变量替代。"}'

  exit 0

fi

# 自动改写:将硬编码的 API key 替换为环境变量引用

SAFE_CMD=$(echo "$COMMAND" | sed 's/sk-[a-zA-Z0-9]\{20,\}/$OPENAI_API_KEY/g')

if [ "$COMMAND" != "$SAFE_CMD" ]; then

  echo "{\"continue\": true, \"hookSpecificOutput\": {\"updatedInput\": {\"command\": $(echo "$SAFE_CMD" | jq -Rs .)}}}"

  exit 0

fi

echo '{}' # 放行

实战场景二:禁止在主分支直接推送

#!/bin/bash

INPUT=$(cat)

COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')

BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)

if [ "$BRANCH" = "main" ] && echo "$COMMAND" | grep -q "git push"; then

  echo '{"continue": true, "decision": "block", "reason": "禁止直接推送到 main 分支。请创建 feature 分支后提 PR。"}'

  exit 0

fi

echo '{}'

实战场景三:禁止危险数据库操作

#!/bin/bash

INPUT=$(cat)

COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')

UPPER=$(echo "$COMMAND" | tr '[:lower:]' '[:upper:]')

if echo "$UPPER" | grep -qE '\b(DROP TABLE|DELETE FROM|TRUNCATE)\b'; then

  echo '{"continue": true, "decision": "block", "reason": "危险 SQL 操作已拦截。如需执行,请手动在终端操作并确认。"}'

  exit 0

fi

echo '{}'

⚠️ 踩坑提醒:PreToolUse 的 updatedInput 要求返回完整的工具输入对象,不是只返回被修改的字段。上面的 jq -Rs . 确保命令字符串被正确 JSON 编码。如果返回格式错误,Hook 会静默失败,工具以原始输入执行——不会报错,但也不会生效

2. UserPromptSubmit — 动态版 CLAUDE.md

触发时机:你每次按 Enter 发送消息时。

它的返回值 additionalContext 会作为系统消息注入到下一个模型推理轮次。这相当于一个按上下文动态变化的 CLAUDE.md——比写死的项目规则文件更灵活。

#!/bin/bash

# user-prompt-submit.sh — 根据用户输入动态注入上下文

INPUT=$(cat)

PROMPT=$(echo "$INPUT" | jq -r '.prompt // ""')

ADDITIONS=""

# 场景1:用户提到 PII 或用户数据时注入合规提醒

if echo "$PROMPT" | grep -qiE '(用户数据|PII|个人信息|隐私)'; then

  ADDITIONS="$ADDITIONS

⚠️ 合规提醒:

- 不得在日志中输出用户真实姓名、手机号、身份证号

- 测试数据必须使用假名和脱敏数据

- 如需处理真实用户数据,请先确认已获得授权"

fi

# 场景2:自动关联相关文件

RELATED=$(grep -l "$(echo "$PROMPT" | head -c 50)" src/**/*.ts 2>/dev/null | head -5 | tr '\n' ',')

if [ -n "$RELATED" ]; then

  ADDITIONS="$ADDITIONS

📁 可能相关的文件: $RELATED"

fi

# 场景3:API 花费阈值检查(示例:从本地监控服务获取当日花费)

SPEND_TODAY=$(cat /tmp/daily-api-spend 2>/dev/null || echo "0")

if [ "$SPEND_TODAY" -gt 50 ]; then

  ADDITIONS="$ADDITIONS

💰 今日 API 花费已达 $${SPEND_TODAY},请优先使用缓存结果,减少重复调用。"

fi

if [ -n "$ADDITIONS" ]; then

  echo "{\"continue\": true, \"hookSpecificOutput\": {\"additionalContext\": $(echo "$ADDITIONS" | jq -Rs .)}}"

else

  echo '{}'

fi

▲ 图2:PreToolUse 安全防护三场景 — 密钥检测替换、主分支推送保护、危险操作拦截▲ ▲ 图2:PreToolUse 安全防护三场景 — 密钥检测替换、主分支推送保护、危险操作拦截

3. PostToolUse — 强制测试执行

触发时机:每次工具调用成功之后。

这个 Hook 的价值在于消除"模型改完代码忘记跑测试"这类重复性错误

#!/bin/bash

# post-tool-use.sh — Edit 操作后自动提醒跑测试

INPUT=$(cat)

TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""')

if [ "$TOOL" = "Edit" ] || [ "$TOOL" = "Write" ]; then

  echo '{"continue": true, "hookSpecificOutput": {"additionalContext": "📋 检测到文件修改操作。请确认是否需要运行相关测试:npm test / cargo test / pytest。"}}'

else

  echo '{}'

fi

进阶用法——从 Bash 输出中脱敏

#!/bin/bash

INPUT=$(cat)

TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""')

OUTPUT=$(echo "$INPUT" | jq -r '.tool_output // ""')

if [ "$TOOL" = "Bash" ]; then

  # 在模型看到输出之前,把 access token 替换为占位符

  SANITIZED=$(echo "$OUTPUT" | sed 's/sk-[a-zA-Z0-9]\{20,\}/[REDACTED]/g')

  if [ "$OUTPUT" != "$SANITIZED" ]; then

    echo "{\"continue\": true, \"hookSpecificOutput\": {\"updatedMCPToolOutput\": $(echo "$SANITIZED" | jq -Rs .)}}"

    exit 0

  fi

fi

echo '{}'

⚠️ 重要:PostToolUse 不能阻止已经发生的事情——它只能影响下一个推理轮次。如果你想在工具执行之前就拦截,必须用 PreToolUse。

4. SessionStart — 会话级别的上下文注入

触发时机:会话启动、恢复、清除、compact 之后。

source 字段告诉你启动类型(startup / resume / clear / compact),可以针对不同场景注入不同上下文。最被低估的功能是 watchPaths——让你注册 cwd 之外的路径来触发 FileChanged 事件。

#!/bin/bash

INPUT=$(cat)

SOURCE=$(echo "$INPUT" | jq -r '.source // "startup"')

case "$SOURCE" in

  "resume")

    # 恢复会话时,注入上次关闭前的关键上下文

    echo '{"continue": true, "hookSpecificOutput": {"additionalContext": "欢迎回来!上次你在开发用户认证模块,当前分支: '"$(git branch --show-current)"'。未完成的 TODO 见 docs/TODO.md"}}'

    ;;

  "compact")

    # compact 之后,保留最重要的项目规则

    echo '{"continue": true, "hookSpecificOutput": {"additionalContext": "📌 核心规则(compact 后保留):\n1. TypeScript strict mode,禁止 any\n2. API 端点用 Zod 校验\n3. 每次 Edit 后运行测试"}}'

    ;;

  *)

    echo '{}'

    ;;

esac

5. PermissionRequest — 分类授权策略

触发时机:工具调用需要向你请求权限时。

与其每次都点"批准"或直接开 YOLO 模式,不如用代码编码授权策略

#!/bin/bash

# permission-request.sh — 分类授权

INPUT=$(cat)

TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""')

COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')

# 安全命令:自动允许

if echo "$COMMAND" | grep -qE '^(ls|cat|grep|git status|git diff|git log|find|wc|head|tail|which|pwd|echo|node -v|npm -v|python --version)'; then

  echo '{"continue": true, "decision": "allow"}'

  exit 0

fi

# 写操作:检查是否在当前仓库内

REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)

if [ -n "$REPO_ROOT" ]; then

  CWD=$(pwd)

  if [[ "$CWD" != "$REPO_ROOT"* ]]; then

    # 写操作发生在仓库外 → 拒绝

    echo '{"continue": true, "decision": "deny", "reason": "写操作超出仓库范围。如果确实需要,请手动执行。"}'

    exit 0

  fi

fi

# 其他情况 → 交给用户决定

echo '{}'

💡 经验之谈:自动允许安全的只读命令(ls、cat、grep、git status 等),模型就不会被这些无风险的权限弹窗打断思路。但同时要在工作仓库外严格限制写操作。

场景化 Hook:需要时再开启

这些 Hook 不是日常必开,但在特定场景下能发挥巨大价值:

Hook最佳场景怎么用
PostToolUseFailure构建/测试失败时自动诊断注入 "Bash 失败原因:端口 3000 被占用,进程 PID 12345" 给模型
PermissionDenied权限被拒后告诉模型为什么返回 additionalContext 说明拒绝原因,或设置 retry: true 让模型换一种方式
Stop防止模型草率收工注入检查清单 "你真的完成了吗?请确认:测试通过?文档更新?没有 TODO?"
PreCompact / PostCompact长对话质量保持在 compact 前后锚定关键事实,防止上下文压缩丢失重要信息
SubagentStop子 Agent 结果聚合将子 Agent 的碎片化结果结构化为父对话可用的格式
InstructionsLoaded调试 "这条规则哪来的?"load_reason 字段告诉你规则来源(session-start、nested 等)

常见踩坑与排障

▲ 图3:PermissionRequest 分类授权策略 — 安全命令自动允许、危险命令检查仓库范▲ ▲ 图3:PermissionRequest 分类授权策略 — 安全命令自动允许、危险命令检查仓库范围后决策

坑1:Hook 脚本退出时没有返回有效 JSON

症状:Hook 配置正确但似乎不生效,Claude Code 没有任何报错。

根因:Claude Code 对非零退出码的处理是"记录但不阻塞"——如果你的脚本因语法错误退出,它不会中断工具调用,而是静默跳过。

排查:先手动测试 Hook 脚本:

echo '{"tool_name":"Bash","tool_input":{"command":"echo test"}}' | bash /path/to/hook.sh

# 确认返回的是有效 JSON

坑2:PreToolUse 的 updatedInput 格式错误

症状:PreToolUse 返回了 updatedInput,但工具仍然以原始参数执行。

根因updatedInput 必须是完整的工具输入对象,不能只返回修改的字段。例如对 Bash 工具,必须返回 {"command": "修改后的命令"}

正确示例

# ✅ 正确:返回完整的 tool_input 对象

echo '{"continue": true, "hookSpecificOutput": {"updatedInput": {"command": "safe-command"}}}'

# ❌ 错误:只返回修改的部分

echo '{"continue": true, "hookSpecificOutput": {"updatedInput": "safe-command"}}'

坑3:Hook 超时设置太短

症状:Hook 在复杂检查时被截断。

建议:涉及网络请求(如 API 花费查询)的 Hook,将 timeout 设置为 10-15 秒。本地文件扫描 2-3 秒足够。

坑4:Hook 脚本路径问题

Claude Code 从自己的 cwd 执行 Hook,不一定是你的项目根目录。始终使用绝对路径或用 $HOME 前缀。

// ❌ 错误

"command": ".claude/hooks/pre-bash.sh"

// ✅ 正确

"command": "/home/user/.claude/hooks/pre-bash.sh"

总结

Hook 体系的核心价值不在任何单个 Hook 上,而在于它重新定义了人与 AI Coding Agent 的关系:

  • 从"告诉模型遵守规则" → "用代码强制规则"
  • 从"手动批准每次调用" → "编码策略自动决策"
  • 从"CLAUDE.md 写死的规则" → "运行时动态注入上下文"

投入 1 小时配置好 PreToolUse + PermissionRequest 两个 Hook,就能解决 80% 的重复性摩擦。剩下的 Hook 按需开启,不需要一次性全部配完。

行动建议

  1. 今天:写一个 PreToolUse Hook,拦截 git push main 和密钥泄露
  2. 本周:配置 PermissionRequest,自动允许 ls/cat/grep/git status
  3. 本月:按场景添加 PostToolUse(强制测试)和 SessionStart(compact 保真)

本文由AI辅助创作,经人工审核编辑发布