Claude Code Hooks:给 AI 助手装上"安全带"

Claude Code Hooks:给 AI 助手装上"安全带"

先说一个真实的痛

你让 AI 帮忙清理项目,它顺手执行了 rm -rf ./node_modules------结果路径拼错,把整个项目目录删了。或者你让它改了三个文件,改完之后忘了跑 lint,提交上去 CI 红了一片。

这些问题本质上都是同一个矛盾:AI 很能干,但它没有你项目里的"规矩" 。它不知道哪些命令不能跑,不知道改完代码要格式化,不知道你的项目架构长什么样。

Hooks 就是解决这个矛盾的机制。它让你在 AI 工作流的关键节点上,插入自己写的逻辑。不是改变 AI 本身,而是在它行动之前/之后,加上你的检查、你的自动化、你的上下文。

一个直觉性的理解: Hooks 之于 Claude Code,类似 Git Hooks 之于 Git。pre-commit 在提交前跑 lint,PreToolUse 在工具执行前做安全检查------思路完全一样。


两套 Hooks,两种玩法

Claude Code 有两套 Hooks 系统,面向不同的使用场景:

CLI Hooks(配置文件) SDK Hooks(代码回调)
在哪配置 settings.json 里写 JSON Python / TypeScript 代码里注册函数
能做什么 跑 Shell 脚本、发 HTTP 请求、调 LLM 做判断 任意代码逻辑
适合谁 日常开发的开发者 构建自定义 AI 应用的人
覆盖事件 核心子集 全部 20 种

大多数人的起点是 CLI Hooks ------在项目里加一个 .claude/settings.json,写几行配置就能跑。SDK Hooks 是给更复杂的场景准备的。这篇文章两条线都会讲到,但主线是 CLI。


全部 Hook 事件一览

20 种事件,分成四类,先有个全貌:

工具调用(最常用)

事件 什么时候触发 能干什么
PreToolUse 工具执行 阻止执行、改参数、注入上下文
PostToolUse 工具执行成功后 注入提示、改输出
PostToolUseFailure 工具执行失败后 错误处理
PostToolBatch 一批工具全跑完后 批量后处理

用户交互

事件 什么时候触发 能干什么
UserPromptSubmit 用户发消息时 改 prompt、注入上下文
Notification 发通知时 自定义通知行为
PermissionRequest 请求权限时 自定义权限决策

会话生命周期

事件 什么时候触发 能干什么
SessionStart 会话启动时 预加载环境、装依赖
SessionEnd 会话结束时 清理、生成报告
Stop Claude 完成回复时 后处理、判断是否真的做完了
SubagentStart / SubagentStop 子代理启停时 初始化、收集结果
Setup 初始化阶段 配置加载

其他高级事件

事件 什么时候触发
PreCompact 上下文压缩前(可以趁机塞重要信息)
TeammateIdle 协作队友空闲时
TaskCompleted 任务完成时
ConfigChange 配置变更时
WorktreeCreate / WorktreeRemove 创建/删除 worktree 时
MessageDisplay 消息渲染时

PostToolBatchMessageDisplay 目前只有 TypeScript SDK 支持。CLI Hooks 覆盖的是最常用的核心事件。


CLI Hooks 怎么配

配置文件放在哪

三个层级,优先级从低到高:

js 复制代码
~/.claude/settings.json          # 你个人的全局配置,所有项目生效
.claude/settings.json            # 项目级,可以提交到 Git,团队共享
.claude/settings.local.json      # 项目本地,不提交 Git,个人覆盖用

一个经验: 安全策略、格式化规则放项目级(团队一起用);个人偏好的通知方式放用户级。

基本结构

js 复制代码
{
  "hooks": {
    "<HookEvent>": [
      {
        "matcher": "Bash|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "your-script.sh"
          }
        ]
      }
    ]
  }
}

关键点:matcher 决定了这个 hook 在哪些工具上触发。支持正则:

js 复制代码
"matcher": "Bash"             // 只匹配 Bash 工具
"matcher": "Edit|Write"       // 匹配编辑和写入
"matcher": "mcp__github__.*"  // 匹配所有 GitHub MCP 工具
// 不写 matcher → 所有工具都触发

还有一个更精细的 if 字段,按工具参数过滤:

js 复制代码
{
  "type": "command",
  "if": "Bash(rm *)",
  "command": ".claude/hooks/block-rm.sh"
}

意思是只有 Bash 工具执行的命令以 rm 开头时,才触发这个 hook。👇 这个功能在做安全策略时非常实用。

三种 Hook 类型

Command(最常用)

执行一个 Shell 命令。输入通过 stdin 传 JSON,输出通过 stdout 返回。

js 复制代码
{
  "type": "command",
  "command": "bash .claude/hooks/my-hook.sh",
  "timeout": 60,
  "async": false
}
  • timeout:超时秒数,默认 60
  • async:设为 true 就不阻塞 Claude,它在后台跑
HTTP

给远程服务发请求。适合接审计系统、安全策略服务之类的外部系统。

js 复制代码
{
  "type": "http",
  "url": "http://localhost:8080/hooks/pre-tool-use",
  "timeout": 30,
  "headers": {
    "Authorization": "Bearer $MY_TOKEN"
  },
  "allowedEnvVars": ["MY_TOKEN"]
}

注意:allowedEnvVars 是一个安全白名单,只有在这里声明的环境变量才可以在 headers 里用。还需要在 settings 里加一级声明:

js 复制代码
{
  "httpHookAllowedEnvVars": ["MY_TOKEN", "HOOK_SECRET"]
}

HTTP hook 默认非阻塞------请求失败不会阻止 Claude 继续工作。

Prompt(让 LLM 做判断)

把 hook 的输入扔给一个轻量模型(默认 Haiku),让它决定该不该继续。适合需要"理解语义"的场景。

js 复制代码
{
  "type": "prompt",
  "prompt": "判断 Claude 是否应该停止:$ARGUMENTS。检查所有任务是否真的完成了。",
  "model": "claude-haiku-4-5-20251001",
  "timeout": 30
}

$ARGUMENTS 会被替换成 hook 的 JSON 输入。用 Haiku 是因为便宜且快,做这类简单判断足够了。


Hook 的输入和输出

这是理解 Hooks 的关键------数据怎么进,怎么出

输入(stdin 收到的 JSON)

每个 hook 都会通过 stdin 收到一个 JSON 对象:

js 复制代码
{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../session.jsonl",
  "cwd": "/Users/.../my-project",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test"
  }
}

不同事件会带不同的字段:

字段 哪些事件有 说明
session_id 全部 当前会话 ID
transcript_path 全部 会话记录文件路径
cwd 全部 当前工作目录
tool_name 工具相关事件 被调用的工具名
tool_input 工具相关事件 工具的输入参数
tool_output PostToolUse 工具执行结果
prompt UserPromptSubmit 用户发的消息
source SessionStart 启动来源:startup / resume / clear / compact

输出(stdout 返回的 JSON)

Hook 脚本的标准输出可以返回 JSON 来影响 Claude 的行为。什么都不输出 + 退出码 0 = "没事,继续"。

PreToolUse 的输出是最强大的,可以控制权限、改参数:

js 复制代码
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "不允许删除 node_modules 以外的目录",
    "updatedInput": {
      "file_path": "/sandbox/new-path.ts"
    },
    "additionalContext": "当前环境: production,请谨慎操作"
  }
}
字段 说明
permissionDecision allow(放行)、deny(阻止)、ask(问用户)、defer(交给默认流程)
permissionDecisionReason 原因,Claude 会看到
updatedInput 替换后的工具参数(比如改文件路径)
additionalContext 额外上下文,Claude 下一步会参考

PostToolUse 和 UserPromptSubmit 的输出简单一些,主要是注入 additionalContext

退出码

退出码 含义
0 正常,没有特殊决策
2 阻止操作 (只在 PreToolUse 有效)
其他非零 出错了,但不会阻止

💡 一个细节: 退出码 2 和 JSON 里的 permissionDecision: "deny" 都能阻止操作。区别在于退出码 2 是"快速拒绝",写脚本时一行 exit 2 搞定;JSON 方式更灵活,能附带原因和修改建议。


Agent SDK Hooks

如果你在构建自己的 AI 应用(而不是日常写代码),可以在代码里直接注册 hook 回调。这比 CLI hooks 灵活得多------完整编程能力,想干什么都行。

Python 写法

js 复制代码
from claude_agent_sdk import query, ClaudeAgentOptions, HookMatcher, HookContext
from typing import Any

async def validate_bash(input_data: dict, tool_use_id: str | None, context: HookContext) -> dict:
    """阻止 rm -rf"""
    if input_data["tool_name"] == "Bash":
        command = input_data["tool_input"].get("command", "")
        if "rm -rf" in command:
            return {
                "hookSpecificOutput": {
                    "hookEventName": "PreToolUse",
                    "permissionDecision": "deny",
                    "permissionDecisionReason": "危险命令已阻止",
                }
            }
    return {}

async def log_usage(input_data: dict, tool_use_id: str | None, context: HookContext) -> dict:
    """记录所有工具调用"""
    print(f"[AUDIT] {input_data.get('tool_name')}")
    return {}

options = ClaudeAgentOptions(
    hooks={
        "PreToolUse": [
            HookMatcher(matcher="Bash", hooks=[validate_bash]),
            HookMatcher(hooks=[log_usage]),
        ],
        "PostToolUse": [HookMatcher(hooks=[log_usage])],
    }
)

async for message in query(prompt="重构 auth 模块", options=options):
    print(message)

TypeScript 写法

js 复制代码
import { query, type HookInput, type HookJSONOutput } from "@anthropic-ai/claude-agent-sdk";

const auditBash = async (input: HookInput): Promise<HookJSONOutput> => {
  if (input.hook_event_name !== "PreToolUse") return {};
  const toolInput = input.tool_input as { command?: string };
  if (toolInput.command?.includes("rm -rf")) {
    return {
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "deny",
        permissionDecisionReason: "危险命令已阻止",
      },
    };
  }
  return {};
};

for await (const message of query({
  prompt: "重构 auth 模块",
  options: {
    hooks: {
      PreToolUse: [{ matcher: "Bash", hooks: [auditBash] }],
    },
  },
})) {
  if (message.type === "result" && message.subtype === "success") {
    console.log(message.result);
  }
}

SDK Hooks 和 CLI Hooks 的核心区别

  1. SDK 覆盖全部 20 种事件,CLI 只有核心子集
  2. SDK 是代码,能查数据库、调 API、做复杂逻辑
  3. SDK 可以链式调用,多个 HookMatcher 按顺序执行
  4. CLI 更轻量,写个 Shell 脚本就能跑,学习成本最低

实战:九个真实场景

理论讲完了,下面是真正有用的部分。

场景 1:阻止危险命令 ✅

问题: AI 执行了不该执行的删除命令。

配置:

js 复制代码
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(rm *)",
            "command": ".claude/hooks/block-rm.sh"
          }
        ]
      }
    ]
  }
}

block-rm.sh

js 复制代码
#!/bin/bash
command=$(jq -r '.tool_input.command' < /dev/stdin)

if [[ "$command" == *"rm -rf"* ]]; then
  echo "Blocked: rm -rf commands are not allowed" >&2
  exit 2  # 退出码 2 = 阻止执行
fi

exit 0

为什么用 if 过滤: 没必要让每次 Bash 调用都跑这个脚本。加了 if: "Bash(rm *)" 之后,只有命令以 rm 开头时才触发,其他命令零开销。

场景 2:自动格式化

问题: AI 改完代码不跑格式化,代码风格不一致。

js 复制代码
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

一行配置,从 stdin 读文件路径,喂给 prettier。改完就格式化,不用你操心。

场景 3:后台跑测试

问题: 想让 AI 改完代码自动跑测试,但不想让它等测试跑完才继续干活。

js 复制代码
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/run-tests.sh",
            "async": true,
            "timeout": 300
          }
        ]
      }
    ]
  }
}

async: true 是关键。Claude 改完文件立刻继续写下一个,测试在后台跑。测试失败了,结果会在后面的对话中通知到。

场景 4:按关键词自动注入上下文

这个是我最推荐的一个用法。

问题: 每次都要手动告诉 AI "你去看看 docs/frontend 下的文档",很烦。

思路: 让 hook 分析用户消息里的关键词,自动把对应的文档喂给 AI。

js 复制代码
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/context-router.sh"
          }
        ]
      }
    ]
  }
}

context-router.sh

js 复制代码
#!/bin/bash
PROMPT=$(cat)

FRONTEND_KEYWORDS="前端|frontend|react|vue|css|component|UI|页面"
BACKEND_KEYWORDS="后端|backend|api|server|database|数据库|接口|service"

CONTEXT=""

if echo "$PROMPT" | grep -qiE "$FRONTEND_KEYWORDS"; then
  DOCS=$(find ./docs/frontend -name "*.md" -exec cat {} ; 2>/dev/null | head -c 8000)
  CONTEXT="检测到前端任务,已加载相关文档:\n$DOCS"
fi

if echo "$PROMPT" | grep -qiE "$BACKEND_KEYWORDS"; then
  DOCS=$(find ./docs/backend -name "*.md" -exec cat {} ; 2>/dev/null | head -c 8000)
  CONTEXT="${CONTEXT}\n检测到后端任务,已加载相关文档:\n$DOCS"
fi

if [ -n "$CONTEXT" ]; then
  jq -n --arg ctx "$CONTEXT" '{"hookSpecificOutput": {"hookEventName": "UserPromptSubmit", "additionalContext": $ctx}}'
fi

本质上是把"你该去读这个"变成"我已经读了,在这儿"。 AI 省了搜索的时间,你省了提醒的口水。

场景 5:桌面通知

问题: AI 在后台跑一个长任务,你想在它需要你的时候知道。

js 复制代码
{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification "Claude Code 需要你的关注" with title "Claude Code"'"
          }
        ]
      }
    ]
  }
}

macOS 用 osascript,Linux 换成 notify-send "Claude Code" "需要你的关注"。简单直接。

场景 6:会话启动时预加载

问题: 每次新开会话都要手动装依赖、初始化环境。

js 复制代码
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|resume",
        "hooks": [
          {
            "type": "command",
            "command": ""$CLAUDE_PROJECT_DIR"/scripts/bootstrap.sh"
          }
        ]
      }
    ]
  }
}

matcher 这里过滤的是启动来源:startup 是全新会话,resume 是恢复会话。你可以选择只在全新会话时跑初始化。

场景 7:审计日志

问题: 需要记录 AI 执行了哪些 Shell 命令,用于安全审计。

js 复制代码
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.command' >> ~/.claude/bash-audit.log"
          }
        ]
      }
    ]
  }
}

这个 hook 什么都不返回,只是静默地把命令追加到日志文件。对 Claude 的行为完全透明。

场景 8:让 AI 自己判断该不该停

问题: AI 说"我做完了",但其实还差一个收尾步骤没做。

js 复制代码
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "评估 Claude 是否应该停止:$ARGUMENTS。检查所有任务是否真的完成了。如果还有未完成的工作,输出阻止决策。"
          }
        ]
      }
    ]
  }
}

用 Prompt 类型,让一个轻量 LLM 审查"AI 说做完了"这个决策。本质上是一个低成本的二次检查。Haiku 的调用成本很低,做这种判断刚好。

场景 9:沙箱路径重定向

问题: 在沙箱环境里跑 AI,需要把所有文件写入操作重定向到安全目录。

js 复制代码
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/redirect-to-sandbox.sh"
          }
        ]
      }
    ]
  }
}

redirect-to-sandbox.sh

js 复制代码
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')

echo "$INPUT" | jq --arg newpath "/sandbox${FILE_PATH}" '{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "updatedInput": {
      "file_path": $newpath
    }
  }
}'

updatedInput 替换了原始的文件路径,AI 以为自己写到了 /src/app.ts,实际写到了 /sandbox/src/app.ts


几条踩过的坑

Hook 脚本要快

Hook 默认是同步阻塞的。你的脚本跑 10 秒,Claude 就干等 10 秒。尽量控制在 5 秒以内。 耗时的操作(测试、构建、部署)务必设 async: true

用 jq 处理 JSON

stdin 传入的是 JSON。在 Shell 脚本里,jq 是最顺手的工具:

js 复制代码
TOOL_NAME=$(jq -r '.tool_name')
COMMAND=$(jq -r '.tool_input.command')
FILE_PATH=$(jq -r '.tool_input.file_path')

出错不等于阻止

Hook 脚本崩了,默认不会 阻止 Claude 继续。这是一个设计选择------hook 是辅助,不是瓶颈。如果你希望出错时阻止操作,显式返回 exit 2 或在 JSON 里设 permissionDecision: "deny"

别让 Hook 做 AI 该做的事

Hook 擅长确定性的自动化 :格式化、阻止、日志、路径重写。不确定的判断("这段代码写得怎么样"、"该不该用这个方案")交给 AI 本身或者 Prompt Hook。别用 Shell 脚本写一个半吊子的代码审查器。


小结

Hooks 解决的核心问题是:怎么让 AI 助手遵守你项目里的规矩。

它能做的事:

  • 安全:阻止危险命令、沙箱重定向
  • 质量:自动格式化、lint、测试
  • 效率:上下文自动注入、环境预加载
  • 可观测:审计日志、桌面通知
  • 智能判断:LLM 做二次检查

从哪里开始?我建议先从场景 2(自动格式化)场景 4(上下文注入) 入手。这两个配置简单、效果明显,能让你马上感受到 Hooks 的价值。


参考资源

相关推荐
小林学AI1 小时前
5 分钟上手!Hermes Agent 插件开发保姆级教程,扩展能力从此开挂
ai编程
lichenyang4531 小时前
HarmonyOS 6.0 ArkUI 循环渲染:ForEach、LazyForEach 和 Repeat 到底怎么选?
前端
Sammyyyyy1 小时前
Google I/O 2026 Antigravity 更新解析与 SDK 实战指南
python·ai编程·servbay
Captaincc2 小时前
置身钉内:一个 AI 办公产品的理想、失焦与组织困境
前端·程序员
阿演2 小时前
DataDjinn 新版本更新:国产数据库支持、连接树体验、AI 查询和表格编辑继续增强
数据库·人工智能·ai·ai编程
零陵上将军_xdr2 小时前
后端转全栈学习-Day6-JavaScript 基础-4
前端·javascript·学习
码农小旋风2 小时前
上下文工程
人工智能·chatgpt·claude
Python私教2 小时前
用 Claude Code 做大型重构不翻车:分批+Git 兜底+验证闭环的实战流程(2026)
git·重构·ai编程·代码重构·工程实践·claude code
无心水2 小时前
【OpenClaw:赚钱】案例19、内容产量5倍、广告收入翻4倍:播客转多平台内容矩阵全自动化实战(OpenAI Whisper + Claude)
java·人工智能·python·ai编程·openclaw·养龙虾·java.time