跳转到主内容
返回博客列表

AI 编程安全指南:用 Hook 机制给 Claude Code 加上"刹车"和"安全带"

前言

“Claude Code 写代码确实快,但它万一执行了个 rm -rf / 怎么办?”

这是很多刚上手 Claude Code 的开发者最真实的焦虑。

AI 编程工具越来越强,自动读文件、写代码、跑命令,效率确实起飞。但问题也随之而来 —— 你没法控制它的每一个动作。它改了代码你希望自动格式化,它执行 Bash 命令你希望做安全检查,它写完文件你希望扫描有没有泄露密钥……

如果你有前端经验,你一定熟悉 Vue 的 mountedbeforeDestroy 这些生命周期钩子;如果你写过后端,Spring 的 @Before@After AOP 切面肯定不陌生。它们的共同本质是:在特定事件节点,注入自定义逻辑。 Claude Code 的 Hook 机制,做的正是这件事。

今天这篇文章,我会基于 Anthropic 官方文档和 GitHub 热门教程 claude-howto,用你最熟悉的前后端概念做类比,带你彻底搞懂 Claude Code 的 Hook 机制,并给出 6 个可以直接复制使用的实战配置。


一、什么是 Hook?一句话讲透

Hook = 在特定事件发生时,自动执行你预设的逻辑。

不管是 Vue、Spring 还是 Claude Code,核心思想一模一样:

框架Hook 机制本质
Vuemounted()updated()组件生命周期的回调函数
ReactuseEffect(() => {}, [])函数组件的副作用钩子
Spring@Before@AfterReturningAOP 切面,拦截方法调用前后
Claude CodePreToolUsePostToolUse工具调用生命周期的事件钩子

用一张图理解 Claude Code 的 Hook 工作流:

看到了吗?这和 Spring AOP 的 @Before → 方法执行 → @After 思路非常相似。


二、Hook 的四种类型

Claude Code 提供了四种 Hook 类型,满足不同场景:

类型说明类比
command执行 Shell 脚本,通过 stdin 接收 JSON,通过 exit code 返回决策最通用,就像 Shell 脚本
httpPOST JSON 到远程 URL,适合对接内部系统像 Webhook,通知你的服务
prompt让 LLM 评估当前状态,返回决策让 AI 自己判断该不该继续
agent启动一个专门的子代理,可以调用工具做多步推理prompt 的加强版,能动手验证

重点区别prompt 只能”想”,agent 能”想”还能”做”。比如你要验证代码是否通过测试,prompt 只能看文本判断,agent 可以真的跑一遍测试。

上下文获取方式:不同类型获取事件上下文的方式不同:

类型上下文来源示例
command通过 stdin 接收 JSON(用 cat 读取)input=$(cat)
httpPOST 请求的 body 中携带 JSON服务端从 request body 解析
prompt模板变量 $ARGUMENTS 自动注入"prompt": "检查任务是否完成。$ARGUMENTS"
agent模板变量 $ARGUMENTS 自动注入"prompt": "跑一遍测试。$ARGUMENTS"

其中 $ARGUMENTS 是 Claude Code 的内置模板变量,运行时会被替换为当前 Hook 事件的 JSON 输入数据(包含会话 ID、工具名、工具参数等字段,具体内容因事件类型而异)。如果 prompt 字符串中没有写 $ARGUMENTS,JSON 输入会自动追加到 prompt 末尾。这样 LLM 就能基于真实的事件上下文做判断,而不是只靠你写的静态 prompt。


三、26 个生命周期事件全景图

Claude Code 目前支持 26 个 Hook 事件,按阶段分为 7 组。先看全景图,有个整体印象:

上图从上到下就是一次 Claude Code 会话的完整生命周期。我按组快速过一下各自的职责:

🔄 会话生命周期(6 个):管理会话本身的状态 —— 启动、结束、加载配置、切换目录、监听文件变化。大多数不可拦截,属于”通知型”事件。

🔧 工具调用生命周期(5 个):这是最核心的一组PreToolUse 在工具执行前拦截(类比 @Before),PostToolUse 在执行后追加上下文(类比 @AfterReturning),PostToolUseFailure 处理异常(类比 @AfterThrowing)。日常 80% 的 Hook 都挂在这里。

🤖 子代理与团队(3 个):当 Claude Code 启动子代理或团队协作时触发。SubagentStop 可以拦截子代理的输出,TeammateIdle 可以在队友空闲时分配新任务。

📝 用户交互(2 个):UserPromptSubmit 是用户提交提示词时的”入口检查”,可以在这里拦截危险操作意图(比如”删库跑路”)。

🛑 停止与收尾(4 个):Stop 是 Claude Code 完成回复时的”出口检查”,可以用 prompt 类型让 AI 自己审查是否真的完成了任务。

🌲 Worktree 与压缩(4 个):管理 Git Worktree 和上下文压缩。PreCompact 可以在压缩前注入需要保留的关键信息。

🔗 MCP 交互(2 个):当 MCP 服务器请求用户输入时触发,适合做输入校验或自动填充。

> 记忆技巧:安全拦截和自动化这块,重点掌握 PreToolUsePostToolUseUserPromptSubmitStop 这四个事件就能覆盖大部分场景。但其他事件同样有实战价值 —— 比如 SessionStart 常被用来注入会话历史上下文,Notification 用来做桌面通知提醒,SubagentStop 用来审查子代理输出。按需取用就好。


四、六个实战场景,直接复制使用

🛡️ 场景一:PreToolUse 拦截危险 Bash 命令

类比:Spring 的 @Before 权限校验切面,方法执行前先检查权限。

在 Claude Code 执行 Bash 命令之前,先检查命令是否危险。

配置 settings.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/pre-tool-check.sh\""
          }
        ]
      }
    ]
  }
}

对应的拦截脚本:

#!/bin/bash
# 文件:.claude/hooks/pre-tool-check.sh

input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command')

# 🚫 直接拦截的危险命令(使用 grep -F 做字面量匹配)
blocked_patterns=(
  "rm -rf /"
  "rm -rf /*"
  ":(){ :|:& };:"
  "mkfs."
  "dd if=/dev/zero"
  "dd if=/dev/random"
)

for pattern in "${blocked_patterns[@]}"; do
  if echo "$command" | grep -qF "$pattern"; then
    echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"⛔ 检测到危险命令: $command\"}}"
    exit 0
  fi
done

# ⚠️ 警告但放行的命令(使用 grep -E 做正则匹配)
warn_patterns=("rm -rf" "git push --force" "git reset --hard" "DROP TABLE" "sudo rm")

for pattern in "${warn_patterns[@]}"; do
  if echo "$command" | grep -qE "$pattern"; then
    echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"additionalContext\":\"⚠️ 警告: 检测到敏感操作 $pattern,但已放行\"}}"
    exit 0
  fi
done

exit 0

✨ 场景二:PostToolUse 自动格式化代码

类比:Vue 的 updated() 钩子,DOM 更新后自动执行副作用。

每次 Claude Code 用 Write 或 Edit 工具修改文件后,自动运行格式化工具:

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

对应的格式化脚本:

#!/bin/bash
# 文件:.claude/hooks/format-code.sh

input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path')
ext="${file_path##*.}"

case "$ext" in
  js|ts|jsx|tsx) npx prettier --write "$file_path" 2>/dev/null ;;
  py) python3 -m black "$file_path" 2>/dev/null ;;
  go) gofmt -w "$file_path" 2>/dev/null ;;
  java) google-java-format -i "$file_path" 2>/dev/null ;;
  rs) rustfmt "$file_path" 2>/dev/null ;;
esac

exit 0

效果:Claude Code 写的代码永远保持格式统一,再也不用人工跑 formatter。

🔍 场景三:PostToolUse 安全扫描

类比:Spring 的 @AfterReturning 日志切面,方法正常返回后记录日志。

扫描 Claude Code 写入的文件,检测是否包含硬编码的密钥和密码。

配置 settings.json

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/security-scan.sh\""
          }
        ]
      }
    ]
  }
}

对应的安全扫描脚本:

#!/bin/bash
# 文件:.claude/hooks/security-scan.sh

input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path')

# 跳过敏感目录
[[ "$file_path" =~ \.env|\.git/ ]] && exit 0

warnings=""

# 检查常见密钥模式(注意:macOS BSD grep 不支持 \x27,用单引号变量代替)
QUOTE="'"
if grep -qE "(password|passwd|pwd)\s*=\s*[\"${QUOTE}][^\"${QUOTE}]+[\"${QUOTE}]" "$file_path" 2>/dev/null; then
  warnings+="⚠️ 发现硬编码密码\n"
fi

if grep -qE "(api_key|apikey|secret_key|access_token)\s*=\s*[\"${QUOTE}][^\"${QUOTE}]+[\"${QUOTE}]" "$file_path" 2>/dev/null; then
  warnings+="⚠️ 发现硬编码 API 密钥\n"
fi

if grep -qE '-----BEGIN (RSA |EC )?PRIVATE KEY-----' "$file_path" 2>/dev/null; then
  warnings+="🚨 发现私钥文件\n"
fi

if [ -n "$warnings" ]; then
  # 以 additionalContext 形式返回,不阻断流程
  echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":\"安全扫描发现以下问题:\n$warnings\"}}"
fi

exit 0

📊 场景四:Stop 用 prompt hook 检查任务完成度

类比:这在 Vue/Spring 中没有直接对应,这是 Claude Code 独有的 —— 让 AI 自己审自己

当 Claude Code 认为自己完成工作时,让 LLM 再评估一遍:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "请评估 Claude 是否完成了用户要求的所有任务。检查以下几点:1) 是否所有文件都已修改 2) 是否有遗漏的功能 3) 测试是否通过。如果任务未完成,说明还差什么。$ARGUMENTS",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

更强大的 agent 版本:真的跑一遍测试来验证:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "agent",
            "prompt": "运行项目测试套件,验证所有测试是否通过。如果有失败的测试,报告失败原因。$ARGUMENTS",
            "timeout": 120
          }
        ]
      }
    ]
  }
}

🔔 场景五:Notification 桌面通知

类比:Vue 的 watch 响应式监听,数据变化时触发回调。

当 Claude Code 需要你的注意时(比如权限弹窗、长时间等待),发送 macOS 桌面通知:

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude Code 需要你的关注\" with title \"Claude Code\" sound name \"default\"'"
          }
        ]
      }
    ]
  }
}

Linux 用户替换为:

"command": "notify-send 'Claude Code' '需要你的关注'"

📝 场景六:UserPromptSubmit 拦截危险提示词

在用户提交提示词时就拦截危险操作意图。

配置 settings.json

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/validate-prompt.sh\""
          }
        ]
      }
    ]
  }
}

对应的提示词校验脚本:

#!/bin/bash
# 文件:.claude/hooks/validate-prompt.sh

input=$(cat)
prompt=$(echo "$input" | jq -r '.prompt')

blocked_keywords=("删除数据库" "drop database" "rm -rf /" "format c:" "删库跑路")

for keyword in "${blocked_keywords[@]}"; do
  if echo "$prompt" | grep -qi "$keyword"; then
    echo "{\"decision\":\"block\",\"reason\":\"⛔ 你的提示词包含危险操作: $keyword,已被安全钩子拦截。\"}" >&2
    exit 2
  fi
done

exit 0

五、配置指南

配置文件位置

Hooks 可以配置在多个位置,优先级从高到低:

位置作用范围是否可提交到 Git
~/.claude/settings.json所有项目否(个人设置)
.claude/settings.json当前项目是(团队共享)
.claude/settings.local.json当前项目否(本地覆盖)

核心配置结构

{
  "hooks": {
    "事件名称": [
      {
        "matcher": "工具名模式",
        "hooks": [
          {
            "type": "command",
            "command": "你的脚本路径",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

Matcher 模式规则

> 注意UserPromptSubmitStopTeammateIdleTaskCreatedTaskCompletedWorktreeCreateWorktreeRemoveCwdChanged 这些事件不支持 matcher,每次触发时都会执行,配置了 matcher 也会被静默忽略。Matcher 主要用于工具类事件(如 PreToolUsePostToolUse 等)。

模式说明示例
精确匹配只匹配指定工具"Write"
多选匹配管道符分隔"Edit|Write"
正则匹配包含特殊字符则按正则解析"^Notebook"
通配符匹配所有"*"""
MCP 工具匹配 MCP 服务器工具"mcp__memory__.*"

Hook 脚本的核心机制

输入:通过 stdin 接收 JSON,包含工具名、参数、会话 ID 等。

输出:通过 exit code 和 JSON stdout 控制行为:

  • exit 0 → 继续(stdout 会被解析为 JSON,可通过 hookSpecificOutput 返回决策)
  • exit 2 → 阻断操作(stderr 会显示为错误信息)
  • 其他 exit code → 非阻断错误,继续执行

环境变量

  • $CLAUDE_PROJECT_DIR — 项目根目录的绝对路径(所有 command hook 均可用)
  • $CLAUDE_ENV_FILE — 用于持久化环境变量的文件路径(仅在 SessionStartCwdChangedFileChanged 事件的 command hook 中可用)

快速上手步骤

# 1. 创建 hooks 目录
mkdir -p .claude/hooks

# 2. 复制你需要的脚本
# (从上面六个场景中选)

# 3. 赋予执行权限
chmod +x .claude/hooks/*.sh

# 4. 配置 settings.json
# (把对应的 JSON 配置加进去)

# 5. 测试 Hook
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | bash .claude/hooks/pre-tool-check.sh
echo $?
# 应该输出 2(被拦截)

总结

Hook 机制是 Claude Code 从”AI 黑盒”变成”可控流程”的关键桥梁。

回顾一下我们今天讲的:

  1. Hook 的本质就是事件驱动的回调,和你熟悉的 Vue 生命周期、Spring AOP 是同一个思路
  2. 四个核心事件覆盖 80% 场景:PreToolUse(执行前拦截)、PostToolUse(执行后处理)、UserPromptSubmit(输入校验)、Stop(完成检查)
  3. 四种 Hook 类型满足不同需求:command(最通用)、http(对接外部系统)、prompt(AI 自评)、agent(AI 自验证)
  4. 六个实战配置可以直接复制到你的项目中使用

正如 Spring AOP 让企业级应用有了统一的事务管理和安全控制,Claude Code 的 Hook 让 AI 编程有了安全网质量门

你不需要每件事都盯着 AI 做 —— 配好 Hook,让它在你设定的规则里自由发挥就好。


参考资料

Anthropic 官方 Hooks 文档: https://code.claude.com/docs/en/hooks

Claude Code Hooks 实战教程(英文): https://github.com/luongnv89/claude-howto/tree/main/06-hooks

Claude Code Hooks 中文翻译版: https://github.com/lhfer/claude-howto-zh-cn/tree/main/06-hooks


欢迎关注公众号 FishTech Notes,一块交流使用心得!