Claude Code SDK #6:Hooks 系统全解——在 Agent 生命周期的每个节点插入你的代码

Claude Code SDK #6:Hooks 系统全解——在 Agent 生命周期的每个节点插入你的代码

Hooks 是 Agent SDK 的「神经系统」——在工具调用前后、会话开始结束、用户提示提交等关键节点插入回调函数,不改动任何 SDK 内部代码,就能拦截危险操作、审计工具调用、动态注入上下文。本篇完整拆解生命周期三层结构(每会话/每轮/每工具)、HookMatcher 配置、五种决策输出、三个典型场景代码示例,以及 Python vs TypeScript 的关键差异。

Claude Code SDK 每日技术拆解
2026. 5. 30. · 09:04
구독 3개 · 콘텐츠 40개

리서치 브리프

Agent 跑起来不难。难的是让它在生产环境里可控、可审计、不出事
你无法改动 Claude 内部的推理逻辑,但你可以在它「即将做某件事」之前拦住它,检查、修改、放行或拒绝。这就是 Hooks 的价值所在。1

什么是 Hooks

Hooks 是注册在 ClaudeAgentOptions 上的回调函数,在 Agent 执行的特定节点被 SDK 自动调用。1
整个机制可以用四步描述:
  1. 事件触发:Agent 即将调用某工具,或会话开始,或执行结束
  2. SDK 收集 Hooks:按事件类型找到所有注册的回调,用 matcher 过滤
  3. 回调函数执行:你的代码拿到事件的完整上下文(工具名、参数、session_id 等)
  4. 返回决策allow 放行、deny 拒绝、ask 要求用户确认、或直接修改工具的输入参数
返回 {} 等价于「不干预,继续」。

生命周期三层结构

SDK 的 Hook 事件按触发频率分三个层次:
每次会话触发一次
  • SessionStart / SessionEnd:会话初始化与结束(仅 TypeScript SDK 支持回调形式)
  • SubagentStart / SubagentStop:子 Agent 启动与完成
每轮对话触发一次
  • UserPromptSubmit:用户提交 Prompt,Claude 处理之前
  • Stop:Agent 本轮执行结束
每次工具调用触发
  • PreToolUse:工具调用前,可以拦截
  • PostToolUse:工具成功后,可注入上下文
  • PostToolUseFailure:工具失败后,可记录或给 Claude 反馈
  • PostToolBatch:一批并行工具全部完成后,下一次模型调用前(仅 TypeScript)
其他事件如 Notification(Agent 状态通知)、PreCompact(上下文压缩前)等则按需注册。1
下图来自官方文档,展示了完整的 Hook 生命周期节点分布:
Hook 生命周期图:SessionStart → 每轮循环(UserPromptSubmit → 工具调用循环:PreToolUse/PostToolUse/PostToolBatch → Stop)→ SessionEnd,以及独立的异步事件
Hook 生命周期节点总览 2

核心 API:HookMatcher + 回调签名

注册一个 Hook 需要两样东西:用哪个事件过滤什么工具
from claude_agent_sdk import ClaudeAgentOptions, HookMatcher

options = ClaudeAgentOptions(
    hooks={
        "PreToolUse": [
            HookMatcher(matcher="Write|Edit", hooks=[my_callback])
        ]
    }
)
HookMatcher 有三个字段:
字段类型说明
matcherstr(正则)过滤工具名,如 "Write|Edit""^mcp__"
hookslist[HookCallback]必填,回调函数列表
timeoutint超时秒数,默认 60
注意matcher 只匹配工具名,不匹配文件路径。要按路径过滤,需要在回调内部检查 tool_input["file_path"]
每个回调函数接收三个参数:
async def my_callback(input_data: dict, tool_use_id: str | None, context) -> dict:
    # input_data 包含 session_id、cwd、hook_event_name、tool_name、tool_input 等
    # tool_use_id 用于关联同一工具调用的 PreToolUse 和 PostToolUse
    # context 目前在 Python SDK 中预留,暂未使用
    return {}  # 返回空对象 = 不干预

五类决策输出

回调返回的对象决定 Agent 下一步怎么走。所有与「当前工具调用」相关的决策都放在 hookSpecificOutput 下。
下图说明了一次 PreToolUse Hook 的完整解析流程——从事件触发、matcher 检查、if 条件匹配,到 hook 处理结果返回给 Claude Code:
PreToolUse Hook 解析流程:事件触发 → matcher 检查是否匹配工具名 → if 条件是否匹配 → hook 处理函数运行 → 返回决策
PreToolUse Hook 解析流程示意 2
# 拒绝工具调用
return {
    "hookSpecificOutput": {
        "hookEventName": input_data["hook_event_name"],
        "permissionDecision": "deny",
        "permissionDecisionReason": "禁止写入 .env 文件",
    }
}

# 自动放行
return {
    "hookSpecificOutput": {
        "hookEventName": input_data["hook_event_name"],
        "permissionDecision": "allow",
    }
}

# 修改工具的输入参数(同时自动放行)
return {
    "hookSpecificOutput": {
        "hookEventName": input_data["hook_event_name"],
        "permissionDecision": "allow",
        "updatedInput": {
            **input_data["tool_input"],
            "file_path": f"/sandbox{original_path}",  # 重定向到沙箱目录
        },
    }
}
permissionDecision 四个取值:
  • "allow":跳过权限提示,直接执行
  • "deny":阻止工具调用,把 permissionDecisionReason 返回给 Claude
  • "ask":弹出用户确认对话框
  • "defer":暂停执行,让调用方通过 --resume 手动处理(仅在 -p 非交互模式下有效)
优先级规则:多个 Hook 同时返回决策时,deny > defer > ask > allow——任意一个 deny 就足以拒绝。
PostToolUse 里还有两个额外字段:
  • additionalContext:在工具结果旁边向 Claude 注入补充信息
  • updatedToolOutput:完全替换工具的输出(Claude 会看到替换后的版本,但工具已经跑完)

三个典型场景

场景一:保护敏感文件

async def protect_sensitive_files(input_data, tool_use_id, context):
    file_path = input_data.get("tool_input", {}).get("file_path", "")

# 禁止修改 .env 和所有密钥文件
    sensitive = [".env", ".env.local", "id_rsa", "credentials.json"]
    if any(file_path.endswith(s) for s in sensitive):
        return {
            "hookSpecificOutput": {
                "hookEventName": input_data["hook_event_name"],
                "permissionDecision": "deny",
                "permissionDecisionReason": f"文件 {file_path} 包含敏感信息,已拒绝修改",
            }
        }
    return {}

options = ClaudeAgentOptions(
    hooks={
        "PreToolUse": [
            HookMatcher(matcher="Write|Edit", hooks=[protect_sensitive_files])
        ]
    }
)

场景二:审计日志(异步,不阻塞 Agent)

每次文件变更都写入审计日志,但不让记录操作拖慢 Agent:
import asyncio
from datetime import datetime

async def audit_file_changes(input_data, tool_use_id, context):
    # 先返回异步信号,Agent 继续执行
    asyncio.create_task(_write_audit(input_data))
    return {"async_": True, "asyncTimeout": 5000}

async def _write_audit(input_data):
    file_path = input_data.get("tool_input", {}).get("file_path", "unknown")
    tool = input_data.get("tool_name", "unknown")
    ts = datetime.now().isoformat()
    with open("./audit.log", "a") as f:
        f.write(f"{ts}\t{tool}\t{file_path}\n")

options = ClaudeAgentOptions(
    hooks={
        "PostToolUse": [
            HookMatcher(matcher="Write|Edit", hooks=[audit_file_changes])
        ]
    }
)
注意:异步 Hook 无法影响决策,不能 denyallow,只能做日志、指标、通知等副作用。

场景三:PostToolUse 向 Claude 注入上下文

async def inject_file_context(input_data, tool_use_id, context):
    file_path = input_data.get("tool_input", {}).get("file_path", "")

# 如果修改的是自动生成文件,告知 Claude 不要直接改,要改源文件
    if "generated" in file_path or file_path.endswith(".pb.go"):
        return {
            "hookSpecificOutput": {
                "hookEventName": input_data["hook_event_name"],
                "additionalContext": f"{file_path} 是自动生成文件,请修改对应的 .proto 源文件后重新生成",
            }
        }
    return {}
这段 additionalContext 会被 Claude 在下一次模型调用时看到,相当于在工具结果旁贴了一张便利贴。

Python vs TypeScript 差异备忘

特性Python SDKTypeScript SDK
SessionStart / SessionEnd 回调❌ 不支持(只能用 settings.json shell hook)✅ 支持
PostToolBatch❌ 不支持✅ 支持
async_ 字段(异步输出)async_ 避开 Python 保留字async
agent_id / agent_type 字段仅在 PreToolUse / PostToolUse / PostToolUseFailure 可用所有事件可用
context 参数预留,暂不可用包含 AbortSignal 用于取消
Python 开发者如果需要 SessionStart 逻辑,可以通过监听 receive_response() 的第一条消息来代替,或在 .claude/settings.json 里配置 shell command hook 并设置 setting_sources=["project"]1
完整事件列表、每个 Hook 的 JSON 输入输出 schema 和 matcher 规则,参见官方 Hooks 参考文档:
콘텐츠 카드를 불러오는 중…

三条实践建议

1. 每个 Hook 独立决策,不依赖其他 Hook 的执行顺序
同事件的多个 Hook 并行执行,完成顺序不确定。写每个回调时假设它是唯一运行的那一个。
2. 生产环境的拦截 Hook 必须捕获异常
未处理的异常会中断 Agent 执行。正确姿势:
async def safe_hook(input_data, tool_use_id, context):
    try:
        result = await do_validation(input_data)
        return result
    except Exception as e:
        # 记录日志,但不让异常传播
        print(f"[hook error] {e}")
        return {}  # 降级为不干预
3. additionalContext 写事实,不写指令
Claude 对「看起来像系统指令」的注入文本有防御机制,可能反而把内容暴露给用户。写法是陈述事实:「当前分支: feat/auth-refactor,有未提交改动」,而不是「你必须先提交再继续」。

下一篇:#7 子 Agent(Subagents) —— 用 AgentDefinition 定义专属 Agent、Agent 工具自动代理调用、父子 Agent 消息流的追踪字段,以及子 Agent 的权限隔离策略。

이 콘텐츠를 둘러싼 관점이나 맥락을 계속 보강해 보세요.

  • 로그인하면 댓글을 작성할 수 있습니다.