Skip to content
FunCoding

Search

Search docs, agents, posts, Skills and MCP servers

用 hooks 拦截和控制智能体行为

在 Agent SDK 里用回调函数拦截智能体事件:hooks 的工作方式、全部可用事件(Python/TypeScript 支持情况)、配置与匹配器、回调输入输出、异步输出,以及修改输入、阻止工具、自动批准、多 hook、子智能体跟踪、HTTP 请求、Slack 通知等示例与常见问题。

Hooks 是在智能体事件(如工具被调用、会话启动或执行停止)发生时运行你代码的回调函数。有了 hooks 你可以:

  • 阻止危险操作:在它们执行之前,如破坏性的 shell 命令或未授权的文件访问
  • 记录和审计:为合规、调试或分析记录每个工具调用
  • 转换输入和输出:清洗数据、注入凭据或重定向文件路径
  • 要求人工批准:对数据库写入或 API 调用这类敏感动作
  • 跟踪会话生命周期:管理状态、清理资源或发送通知

hooks 如何工作

  1. 事件触发。 智能体执行期间发生了什么事,SDK 就触发一个事件:工具即将被调用(PreToolUse)、工具返回了结果(PostToolUse)、子智能体启动或停止、智能体空闲,或执行结束(完整事件列表见下)。
  2. SDK 收集已注册的 hooks。 SDK 检查为该事件类型注册的 hooks,包括你在 options.hooks 里传入的回调 hooks,以及设置文件里的 shell 命令 hooks(当对应的 settingSources 或 setting_sources 条目启用时,默认的 query() 选项是启用的)。
  3. 匹配器过滤哪些 hooks 运行。 如果 hook 有 matcher 模式(如 "Write|Edit"),SDK 会把它与事件的目标(例如工具名)比较;没有匹配器的 hooks 对该类型的每个事件都运行。
  4. 回调函数执行。 每个匹配的 hook 的回调函数接收关于发生了什么的输入:工具名、它的参数、会话 ID 和其他事件专属细节。
  5. 你的回调返回决定。 在执行任何操作(记录、API 调用、校验)之后,你的回调返回一个输出对象,告诉智能体该做什么:允许该操作、阻止它、修改输入,或向对话注入上下文。

下面的例子把这些步骤放在一起。它用 "Write|Edit" 匹配器注册一个 PreToolUse hook(步骤 1 和 3),使回调只对写文件工具触发。触发时,回调接收工具的输入(步骤 4),检查文件路径是否指向 .env 文件,并返回 permissionDecision: "deny" 阻止该操作(步骤 5):

import asyncio
from claude_agent_sdk import (
    AssistantMessage,
    ClaudeSDKClient,
    ClaudeAgentOptions,
    HookMatcher,
    ResultMessage,
)


# 定义接收工具调用细节的 hook 回调
async def protect_env_files(input_data, tool_use_id, context):
    # 从工具的输入参数里提取文件路径
    file_path = input_data["tool_input"].get("file_path", "")
    file_name = file_path.split("/")[-1]

    # 目标是 .env 文件时阻止该操作
    if file_name == ".env":
        return {
            "hookSpecificOutput": {
                "hookEventName": input_data["hook_event_name"],
                "permissionDecision": "deny",
                "permissionDecisionReason": "Cannot modify .env files",
            }
        }

    # 返回空对象以允许该操作
    return {}


async def main():
    options = ClaudeAgentOptions(
        hooks={
            # 为 PreToolUse 事件注册 hook
            # 匹配器只过滤出 Write 和 Edit 工具调用
            "PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[protect_env_files])]
        }
    )

    async with ClaudeSDKClient(options=options) as client:
        await client.query("Create a .env file with the standard local development database configuration")

        async for message in client.receive_response():
            # 过滤出助手和结果消息
            if isinstance(message, (AssistantMessage, ResultMessage)):
                print(message)


asyncio.run(main())
import { query, HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";

// 用 HookCallback 类型定义 hook 回调
const protectEnvFiles: HookCallback = async (input, toolUseID, { signal }) => {
  // 把 input 转成具体的 hook 类型以获得类型安全
  const preInput = input as PreToolUseHookInput;

  // 转换 tool_input 以访问它的属性(在 SDK 里类型为 unknown)
  const toolInput = preInput.tool_input as Record<string, unknown>;
  const filePath = toolInput?.file_path as string;
  const fileName = filePath?.split("/").pop();

  // 目标是 .env 文件时阻止该操作
  if (fileName === ".env") {
    return {
      hookSpecificOutput: {
        hookEventName: preInput.hook_event_name,
        permissionDecision: "deny",
        permissionDecisionReason: "Cannot modify .env files"
      }
    };
  }

  // 返回空对象以允许该操作
  return {};
};

for await (const message of query({
  prompt: "Create a .env file with the standard local development database configuration",
  options: {
    hooks: {
      // 为 PreToolUse 事件注册 hook
      // 匹配器只过滤出 Write 和 Edit 工具调用
      PreToolUse: [{ matcher: "Write|Edit", hooks: [protectEnvFiles] }]
    }
  }
})) {
  // 过滤出助手和结果消息
  if (message.type === "assistant" || message.type === "result") {
    console.log(message);
  }
}

运行任一脚本时,Claude 尝试创建 .env 文件,hook 拒绝该工具调用,Claude 的最终响应解释说它不能创建 .env 文件。

可用的 hooks

SDK 为智能体执行的不同阶段提供 hooks。有些 hook 在两个 SDK 里都可用,有些只在 TypeScript 里有。

Hook 事件Python SDKTypeScript SDK什么触发它示例用途
PreToolUse是是工具调用请求(可阻止或修改)阻止危险的 shell 命令
PostToolUse是是工具执行结果把所有文件改动记入审计轨迹
PostToolUseFailure是是工具执行失败处理或记录工具错误
PostToolBatch否是一整批工具调用解决之后,每批在下一次模型调用前触发一次为整批工具调用只注入一次约定
UserPromptSubmit是是用户提交提示向提示里注入额外上下文
UserPromptExpansion否是用户键入的命令或 MCP 提示在到达 Claude 之前展开成提示;Claude 自己调用 skill 时不触发阻止命令被直接调用,或在键入 skill 时添加上下文
MessageDisplay否是带文本的助手消息完成,每条消息一次,携带完整消息文本在不改变记录的情况下编辑或重新格式化显示的文本
Stop是是智能体执行停止退出前保存会话状态
StopFailure否是轮次以 API 错误而不是正常停止结束记录失败或发送告警
SubagentStart是是子智能体初始化跟踪并行任务的派生
SubagentStop是是子智能体完成汇总并行任务的结果
PreCompact是是对话压缩请求在摘要之前归档完整记录
PostCompact否是对话压缩完成记录生成的摘要
PreModelSwitch否是请求的模型切换,在发生之前(可阻止)阻止切换到特定模型
PostModelSwitch否是会话的模型改变,包括自动回退给 Claude 针对新模型的指导
PermissionRequest是是工具调用需要权限决定自定义权限处理
PermissionDenied否是auto 模式拒绝工具调用,包括没有分类器裁决的拒绝记录拒绝,或告诉模型可以重试(对没有裁决的拒绝,Claude Code 忽略 retry: true)
SessionStart否是会话初始化初始化日志和遥测
SessionEnd否是会话终止清理临时资源
Notification是是智能体状态消息把智能体状态更新发到 Slack 或 PagerDuty
Setup否是会话设置/维护运行初始化任务
TeammateIdle否是队友变为空闲重新分配工作或通知
TaskCreated否是通过 TaskCreate 工具创建任务强制任务命名约定
TaskCompleted否是任务被标记为完成要求测试通过后才能关闭任务
Elicitation否是MCP 服务器在任务中途请求用户输入以编程方式响应 MCP 输入请求
ElicitationResult否是用户响应 MCP elicitation在响应返回服务器之前修改或阻止它
ConfigChange否是配置文件变化动态重新加载设置
InstructionsLoaded否是CLAUDE.md 或规则文件被加载进上下文审计加载了哪些说明文件
WorktreeCreate否是创建 git worktree跟踪隔离的工作区
WorktreeRemove否是移除 git worktree清理工作区资源
CwdChanged否是会话期间工作目录改变按目录重新加载环境变量
FileChanged否是被监视的文件被修改、创建或删除项目文件变化时重新加载配置
DirectoryAdded否是会话期间添加了一个工作目录为会话中途添加的仓库安装依赖

配置 hooks

要配置 hook,把它传在智能体选项的 hooks 字段里(Python 里是 ClaudeAgentOptions,TypeScript 里是 options 对象)。这个片段假定你已经定义了 hook 回调,如上面例子里 Python 的 protect_env_files 或 TypeScript 的 protectEnvFiles:

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

async with ClaudeSDKClient(options=options) as client:
    await client.query("Your prompt")
    async for message in client.receive_response():
        print(message)
for await (const message of query({
  prompt: "Your prompt",
  options: {
    hooks: {
      PreToolUse: [{ matcher: "Bash", hooks: [myCallback] }]
    }
  }
})) {
  console.log(message);
}

hooks 选项在 Python 里是字典、在 TypeScript 里是对象:键是 'PreToolUse'、'PostToolUse'、'Stop' 这样的 hook 事件名;值是匹配器数组,每个匹配器包含可选的过滤模式和你的回调函数。

匹配器

用匹配器过滤你的回调何时触发。matcher 字段匹配的值随 hook 事件类型而不同:例如基于工具的 hooks 匹配工具名,而 Notification hooks 匹配通知类型。SDK 匹配器遵循与设置文件里的匹配器相同的规则(那一节记录了精确字符串和正则表达式的评估路径、版本要求以及每种事件类型的匹配器值)。

选项类型默认说明
matcherstringundefined与事件的过滤字段匹配的模式,遵循设置文件里匹配器的规则。对工具 hooks,它是工具名:内置工具包括 Bash、Read、Write、Edit、Glob、Grep、WebFetch、Agent 等(完整列表见「工具输入类型」);MCP 工具使用 mcp__<server>__<action> 模式,其中 <server> 是你在 mcpServers 配置里使用的键
hooksHookCallback[]-必填。模式匹配时要执行的回调函数数组
timeoutnumberundefined超时秒数;省略时 Claude Code 应用该事件的默认超时;你的 SDK 回调遵循 command hook 的默认值

尽可能用 matcher 模式瞄准特定工具:'Bash' 匹配器只对 Bash 命令运行,而省略模式会让你的回调对该事件的每次出现都运行;要记录会话发出的每个工具调用时才有意地省略它。

回调函数

输入。 每个 hook 回调接收三个参数:

  • 输入数据: 包含事件细节的类型化对象,每种 hook 类型有自己的输入形状:例如 PreToolUseHookInput 含 tool_name 和 tool_input,而 NotificationHookInput 含 message(完整类型定义见 TypeScript 和 Python SDK 参考)。所有 hook 输入都共享 session_id、cwd 和 hook_event_name;agent_id 和 agent_type 在 hook 于子智能体内触发时填充:在 TypeScript 里它们在基础 hook 输入上,对所有 hook 类型可用;在 Python 里它们是 PreToolUse、PostToolUse、PostToolUseFailure 和 PermissionRequest 上的可选字段,是 SubagentStart 和 SubagentStop 上的必填字段。
  • 工具使用 ID(str | None / string | undefined):关联同一个工具调用的 PreToolUse 和 PostToolUse 事件。
  • 上下文: 在 TypeScript 里,含用于取消的 signal 属性(AbortSignal);在 Python 里,这个参数保留供将来使用。

输出。 你的回调返回一个包含两类字段的对象:

  • 顶层字段在每个事件上都被接受:systemMessage 向用户显示消息,continue(Python 里是 continue_)决定该 hook 之后智能体是否继续运行;有些事件会丢弃它们或把它们交付到别处(每个事件的章节在 hooks 页上说明它们落在哪里)。
  • hookSpecificOutput 控制当前操作,你在里面设置的字段取决于 hook 事件类型:对 PreToolUse hooks,这里设 permissionDecision("allow"、"deny"、"ask" 或 "defer")、permissionDecisionReason 和 updatedInput(返回 "defer" 会结束查询,以便你之后恢复它);对 PostToolUse hooks,可以设 additionalContext 把信息附加到工具结果上,要在 Claude 看到之前替换工具的输出,设 updatedToolOutput(在两个 SDK 里对任何工具都有效),较旧的 updatedMCPToolOutput 字段只替换 MCP 工具输出且已弃用;在 TypeScript SDK 里,PostToolUse 回调还可以返回 classifierContext,一条关于该工具调用结果的、给 auto 模式权限分类器的简短说明(因为你的回调运行在你应用自己的进程里,分类器可能把你在说明里转述的用户陈述当作用户意图;该字段需要 TypeScript Agent SDK v0.3.236 及以上;长度上限、仅同步的规则以及说明里不该放什么,见「为 auto 模式分类器标注结果」)。

返回 {} 表示不做更改地允许该操作。SDK 回调 hooks 使用与 Claude Code shell 命令 hooks 相同的 JSON 输出格式(后者记录了每个字段和事件专属选项);SDK 类型定义见 TypeScript 和 Python SDK 参考。

当多个 hooks 或权限规则适用时,deny 优先于 defer,defer 优先于 ask,ask 优先于 allow。如果任何 hook 返回 deny,不论其他 hooks 如何,该操作都被阻止。

异步输出。 默认智能体会等你的 hook 返回之后才继续。如果你的 hook 执行副作用(如记录日志或发送 webhook)而不需要影响智能体的行为,可以改为返回异步输出:这告诉智能体立即继续,不等 hook 完成。在这个片段里,Python 的 send_to_logging_service 和 TypeScript 的 sendToLoggingService 代表你定义的任何日志函数:

async def async_hook(input_data, tool_use_id, context):
    # 启动后台任务,然后立即返回
    asyncio.create_task(send_to_logging_service(input_data))
    return {"async_": True, "asyncTimeout": 30000}
const asyncHook: HookCallback = async (input, toolUseID, { signal }) => {
  // 启动后台任务,然后立即返回
  sendToLoggingService(input).catch(console.error);
  return { async: true, asyncTimeout: 30000 };
};
字段类型说明
asynctrue表示异步模式,智能体不等待就继续;在 Python 里用 async_ 以避开保留字
asyncTimeoutnumber后台操作的可选超时毫秒数

异步输出无法阻止、修改或向操作注入上下文,因为智能体已经继续了;只把它们用于记录、指标或通知这类副作用。

示例

本节的几个示例只显示回调函数。要运行其中之一,把回调注册在选项 hooks 字段里匹配的事件下(如「配置 hooks」所示)。

修改工具输入

这个例子拦截 Write 工具调用,并改写 file_path 参数,在前面加上 /sandbox,把所有文件写入重定向到沙盒目录。回调返回带修改后路径的 updatedInput 和 permissionDecision: 'allow' 来自动批准被改写的操作:

async def redirect_to_sandbox(input_data, tool_use_id, context):
    if input_data["hook_event_name"] != "PreToolUse":
        return {}

    if input_data["tool_name"] == "Write":
        original_path = input_data["tool_input"].get("file_path", "")
        return {
            "hookSpecificOutput": {
                "hookEventName": input_data["hook_event_name"],
                "permissionDecision": "allow",
                "updatedInput": {
                    **input_data["tool_input"],
                    "file_path": f"/sandbox{original_path}",
                },
            }
        }
    return {}
const redirectToSandbox: HookCallback = async (input, toolUseID, { signal }) => {
  if (input.hook_event_name !== "PreToolUse") return {};

  const preInput = input as PreToolUseHookInput;
  const toolInput = preInput.tool_input as Record<string, unknown>;
  if (preInput.tool_name === "Write") {
    const originalPath = toolInput.file_path as string;
    return {
      hookSpecificOutput: {
        hookEventName: preInput.hook_event_name,
        permissionDecision: "allow",
        updatedInput: {
          ...toolInput,
          file_path: `/sandbox${originalPath}`
        }
      }
    };
  }
  return {};
};

把 updatedInput 与 permissionDecision: 'allow' 配对来自动批准修改后的输入,或与 permissionDecision: 'ask' 配对把它展示给用户。省略 permissionDecision 时,修改后的输入仍然适用,并流经正常的权限评估;用 'defer' 时 updatedInput 被忽略。始终返回新对象,而不是改动原来的 tool_input。要确认重定向,把前缀设为你能写入的路径,如 ./sandbox 或 /tmp/sandbox(macOS 不允许创建根级的 /sandbox 目录),然后让智能体写一个文件:消息流里 Write 工具的结果点名的是带你沙盒前缀的路径,而不是 Claude 请求的那个。

添加上下文并阻止工具

这个例子阻止对 /etc 目录的写入,并向模型和用户解释原因:permissionDecision: 'deny' 停止工具调用;permissionDecisionReason 告诉模型为什么,使它避免重试;systemMessage 向用户显示发生了什么。

async def block_etc_writes(input_data, tool_use_id, context):
    file_path = input_data["tool_input"].get("file_path", "")

    if file_path.startswith("/etc"):
        return {
            # 顶层字段:显示给用户的消息
            "systemMessage": "Remember: system directories like /etc are protected.",
            # hookSpecificOutput:阻止该操作
            "hookSpecificOutput": {
                "hookEventName": input_data["hook_event_name"],
                "permissionDecision": "deny",
                "permissionDecisionReason": "Writing to /etc is not allowed",
            },
        }
    return {}
const blockEtcWrites: HookCallback = async (input, toolUseID, { signal }) => {
  const preInput = input as PreToolUseHookInput;
  const toolInput = preInput.tool_input as Record<string, unknown>;
  const filePath = toolInput?.file_path as string;

  if (filePath?.startsWith("/etc")) {
    return {
      // 顶层字段:显示给用户的消息
      systemMessage: "Remember: system directories like /etc are protected.",
      // hookSpecificOutput:阻止该操作
      hookSpecificOutput: {
        hookEventName: preInput.hook_event_name,
        permissionDecision: "deny",
        permissionDecisionReason: "Writing to /etc is not allowed"
      }
    };
  }
  return {};
};

要确认阻止生效,把回调注册在 PreToolUse 下并带 Write|Edit 匹配器,让智能体在 /etc 下创建文件:消息流里 Write 工具的结果含 Writing to /etc is not allowed,且没有创建任何文件。

自动批准特定工具

默认情况下,智能体在使用某些工具之前可能提示权限。这个例子通过返回 permissionDecision: 'allow' 自动批准只读文件系统工具(Read、Glob、Grep),让它们无需用户确认就运行,同时让所有其他工具仍受正常权限检查约束:

async def auto_approve_read_only(input_data, tool_use_id, context):
    if input_data["hook_event_name"] != "PreToolUse":
        return {}

    read_only_tools = ["Read", "Glob", "Grep"]
    if input_data["tool_name"] in read_only_tools:
        return {
            "hookSpecificOutput": {
                "hookEventName": input_data["hook_event_name"],
                "permissionDecision": "allow",
                "permissionDecisionReason": "Read-only tool auto-approved",
            }
        }
    return {}
const autoApproveReadOnly: HookCallback = async (input, toolUseID, { signal }) => {
  if (input.hook_event_name !== "PreToolUse") return {};

  const preInput = input as PreToolUseHookInput;
  const readOnlyTools = ["Read", "Glob", "Grep"];
  if (readOnlyTools.includes(preInput.tool_name)) {
    return {
      hookSpecificOutput: {
        hookEventName: preInput.hook_event_name,
        permissionDecision: "allow",
        permissionDecisionReason: "Read-only tool auto-approved"
      }
    };
  }
  return {};
};

注册多个 hooks

事件触发时,所有匹配的 hooks 并行运行。对权限决定,最严格的结果适用:只要有一个 deny,不论其他 hooks 返回什么,工具调用都被阻止。因为完成顺序是不确定的,要让每个 hook 独立行动,而不是依赖另一个 hook 先运行。下面的例子对每个工具调用注册三个独立的检查(其中的 hook 名,如 Python 的 audit_logger 或 TypeScript 的 auditLogger,代表你定义的回调):

options = ClaudeAgentOptions(
    hooks={
        "PreToolUse": [
            HookMatcher(hooks=[authorization_check]),
            HookMatcher(hooks=[input_validator]),
            HookMatcher(hooks=[audit_logger]),
        ]
    }
)
const options = {
  hooks: {
    PreToolUse: [
      { hooks: [authorizationCheck] },
      { hooks: [inputValidator] },
      { hooks: [auditLogger] }
    ]
  }
};

用多工具匹配器过滤

用多工具匹配器让一个回调在相关的工具之间共享。这个例子注册三个不同范围的匹配器(其中每个 hook 名代表你定义的回调):竖线分隔的精确列表(Write|Edit|NotebookEdit)只对文件修改工具触发 file_security_hook;正则(^mcp__)对名字以 mcp__ 开头的任何 MCP 工具触发 mcp_audit_hook;省略的匹配器对每个工具调用(不论名字)触发 global_logger。

options = ClaudeAgentOptions(
    hooks={
        "PreToolUse": [
            # 匹配文件修改工具
            HookMatcher(matcher="Write|Edit|NotebookEdit", hooks=[file_security_hook]),
            # 匹配所有 MCP 工具
            HookMatcher(matcher="^mcp__", hooks=[mcp_audit_hook]),
            # 匹配一切(没有匹配器)
            HookMatcher(hooks=[global_logger]),
        ]
    }
)
const options = {
  hooks: {
    PreToolUse: [
      // 匹配文件修改工具
      { matcher: "Write|Edit|NotebookEdit", hooks: [fileSecurityHook] },
      // 匹配所有 MCP 工具
      { matcher: "^mcp__", hooks: [mcpAuditHook] },
      // 匹配一切(没有匹配器)
      { hooks: [globalLogger] }
    ]
  }
};

跟踪子智能体活动

用 SubagentStop hooks 监控子智能体何时完成工作(完整的输入类型见 TypeScript 和 Python SDK 参考)。这个例子在每次子智能体完成时记录摘要:

async def subagent_tracker(input_data, tool_use_id, context):
    # 子智能体结束时记录它的细节
    print(f"[SUBAGENT] Completed: {input_data['agent_id']}")
    print(f"  Transcript: {input_data['agent_transcript_path']}")
    print(f"  Tool use ID: {tool_use_id}")
    print(f"  Stop hook active: {input_data.get('stop_hook_active')}")
    return {}


options = ClaudeAgentOptions(
    hooks={"SubagentStop": [HookMatcher(hooks=[subagent_tracker])]}
)
import { HookCallback, SubagentStopHookInput } from "@anthropic-ai/claude-agent-sdk";

const subagentTracker: HookCallback = async (input, toolUseID, { signal }) => {
  // 转成 SubagentStopHookInput 以访问子智能体专属字段
  const subInput = input as SubagentStopHookInput;

  // 子智能体结束时记录它的细节
  console.log(`[SUBAGENT] Completed: ${subInput.agent_id}`);
  console.log(`  Transcript: ${subInput.agent_transcript_path}`);
  console.log(`  Tool use ID: ${toolUseID}`);
  console.log(`  Stop hook active: ${subInput.stop_hook_active}`);
  return {};
};

const options = {
  hooks: {
    SubagentStop: [{ hooks: [subagentTracker] }]
  }
};

要确认 hook 触发,注册回调并让智能体把一个小任务委派给子智能体(如列出当前目录里的文件):子智能体结束时,回调打印带子智能体 ID 和记录路径的 [SUBAGENT] Completed: 行。

在 hooks 里发 HTTP 请求

Hooks 可以执行 HTTP 请求这样的异步操作;要在 hook 内部捕获错误,而不是让它们传播。这个例子在每个工具完成后发送 webhook,记录运行了哪个工具以及何时运行;hook 会捕获 webhook 失败的错误:

import asyncio
import json
import urllib.request
from datetime import datetime


def _send_webhook(tool_name):
    """把工具使用数据 POST 到外部 webhook 的同步辅助函数。"""
    data = json.dumps(
        {
            "tool": tool_name,
            "timestamp": datetime.now().isoformat(),
        }
    ).encode()

    req = urllib.request.Request(
        "https://api.example.com/webhook",
        data=data,
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    urllib.request.urlopen(req)


async def webhook_notifier(input_data, tool_use_id, context):
    # 只在工具完成之后触发(PostToolUse),不是之前
    if input_data["hook_event_name"] != "PostToolUse":
        return {}

    try:
        # 在线程里运行阻塞的 HTTP 调用,避免阻塞事件循环
        await asyncio.to_thread(_send_webhook, input_data["tool_name"])
    except Exception as e:
        # 记录错误但不抛出
        print(f"Webhook request failed: {e}")

    return {}
import { query, HookCallback, PostToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";

const webhookNotifier: HookCallback = async (input, toolUseID, { signal }) => {
  // 只在工具完成之后触发(PostToolUse),不是之前
  if (input.hook_event_name !== "PostToolUse") return {};

  try {
    await fetch("https://api.example.com/webhook", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        tool: (input as PostToolUseHookInput).tool_name,
        timestamp: new Date().toISOString()
      }),
      // 传入 signal,使 hook 超时时请求被取消
      signal
    });
  } catch (error) {
    // 把取消与其他错误分开处理
    if (error instanceof Error && error.name === "AbortError") {
      console.log("Webhook request cancelled");
    }
    // 不要重新抛出
  }

  return {};
};

// 注册为 PostToolUse hook
for await (const message of query({
  prompt: "Refactor the auth module",
  options: {
    hooks: {
      PostToolUse: [{ hooks: [webhookNotifier] }]
    }
  }
})) {
  console.log(message);
}

要确认 hook 触发,把 webhook URL 指向你能观察的端点并发送一个使用工具的提示:hook 在每个工具完成后发送带工具名和时间戳的 POST。

把通知转发到 Slack

用 Notification hooks 接收智能体的系统通知并转发到外部服务。在 SDK 会话里,Claude Code 对下列通知类型运行这个 hook:permission_prompt——权限请求在你的 canUseTool 回调上等了约六秒之后(需要 TypeScript Agent SDK v0.3.233 及以上,或 Python Agent SDK v0.2.139 及以上);elicitation_complete 和 elicitation_response——用于用户提示的 elicitation 流程。Claude Code 从 SDK 会话不运行的交互式界面发出其他类型,如 idle_prompt、auth_success 和 elicitation_dialog。每个通知包含带人类可读描述的 message 字段,以及可选的 title。这个例子把每个通知转发到 Slack 频道,需要 Slack 传入 webhook URL(通过给你的 Slack 工作区添加应用并启用传入 webhook 来创建):

import asyncio
import json
import urllib.request

from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, HookMatcher


def _send_slack_notification(message):
    """经传入 webhook 向 Slack 发消息的同步辅助函数。"""
    data = json.dumps({"text": f"Agent status: {message}"}).encode()

    req = urllib.request.Request(
        "https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
        data=data,
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    urllib.request.urlopen(req)


async def notification_handler(input_data, tool_use_id, context):
    try:
        # 在线程里运行阻塞的 HTTP 调用,避免阻塞事件循环
        await asyncio.to_thread(_send_slack_notification, input_data.get("message", ""))
    except Exception as e:
        print(f"Failed to send notification: {e}")

    # 返回空对象。Notification hooks 不修改智能体行为
    return {}


async def main():
    options = ClaudeAgentOptions(
        hooks={
            # 为 Notification 事件注册 hook(不需要匹配器)
            "Notification": [HookMatcher(hooks=[notification_handler])],
        },
    )

    async with ClaudeSDKClient(options=options) as client:
        await client.query("Analyze this codebase")
        async for message in client.receive_response():
            print(message)


asyncio.run(main())
import { query, HookCallback, NotificationHookInput } from "@anthropic-ai/claude-agent-sdk";

// 定义把通知发到 Slack 的 hook 回调
const notificationHandler: HookCallback = async (input, toolUseID, { signal }) => {
  // 转成 NotificationHookInput 以访问 message 字段
  const notification = input as NotificationHookInput;

  try {
    // 把通知消息 POST 到 Slack 传入 webhook
    await fetch("https://hooks.slack.com/services/YOUR/WEBHOOK/URL", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        text: `Agent status: ${notification.message}`
      }),
      // 传入 signal,使 hook 超时时请求被取消
      signal
    });
  } catch (error) {
    if (error instanceof Error && error.name === "AbortError") {
      console.log("Notification cancelled");
    } else {
      console.error("Failed to send notification:", error);
    }
  }

  // 返回空对象。Notification hooks 不修改智能体行为
  return {};
};

// 为 Notification 事件注册 hook(不需要匹配器)
for await (const message of query({
  prompt: "Analyze this codebase",
  options: {
    hooks: {
      Notification: [{ hooks: [notificationHandler] }]
    }
  }
})) {
  console.log(message);
}

Notification 事件触发时,hook 把通知的 message(加前缀 Agent status:)发到你的 webhook 所指向的频道。

排查常见问题

hook 没有触发

  • 确认 hook 事件名正确且区分大小写(PreToolUse,不是 preToolUse)
  • 检查你的匹配器模式与工具名完全匹配
  • 确保 hook 在 options.hooks 里的事件类型是正确的
  • 对支持匹配器的非工具 hooks(如 Notification 和 SubagentStop),匹配器匹配的是不同的字段,而 Stop 完全忽略匹配器(见匹配器模式)
  • 智能体达到 max_turns 限制时 hooks 可能不触发,因为会话在 hooks 能执行之前就结束了

匹配器没有按预期过滤

匹配器只匹配工具名,不匹配文件路径或其他参数。要按文件路径过滤,在 hook 内部检查 tool_input.file_path:

const myHook: HookCallback = async (input, toolUseID, { signal }) => {
  const preInput = input as PreToolUseHookInput;
  const toolInput = preInput.tool_input as Record<string, unknown>;
  const filePath = toolInput?.file_path as string;
  if (!filePath?.endsWith(".md")) return {}; // 跳过非 markdown 文件

  // 处理 markdown 文件...
  return {};
};

hook 超时

Claude Code 对每个回调带超时运行,你用其 HookMatcher 上的 timeout 字段以秒为单位设置它。没设置时,Claude Code 用该事件的默认值:多数事件 600 秒,UserPromptSubmit、PreModelSwitch 和 PostModelSwitch 为 30 秒,MessageDisplay 为 10 秒。Claude Code 在关闭期间按更短的 SessionEnd 超时预算(默认 1.5 秒)运行 SessionEnd 回调。回调超过超时时,Claude Code 取消它并丢弃它的输出,会话继续而不是挂起;接下来发生什么取决于事件:

  • PreToolUse:Claude Code 不运行该工具调用,Claude 收到说明 hook 在超时前没有响应的工具结果,轮次继续;如果另一个 PreToolUse hook 返回了显式 deny,Claude 收到的是那个拒绝而不是超时错误(v2.1.210 之前,Claude Code 把超时作为用户拒绝报告给 Claude,这使无人值守的会话停下来等待输入)。
  • PostToolUse 和 PostToolUseFailure:Claude Code 保留工具结果,轮次继续。
  • UserPromptSubmit 和 UserPromptExpansion:Claude Code 用点名 hook 和超时的消息阻止该提示,会话继续;因为这些事件上的回调可以充当策略关卡,Claude Code 从不让超时的提示未经筛查就通过(v2.1.208 之前,这些事件上的回调超时时 Claude Code 以 error_during_execution 结束查询)。
  • Stop 和 SubagentStop:超时的回调算作没有返回决定;智能体或子智能体像该回调允许了那样停止,你其他 hooks 在该事件上的决定仍然适用(Claude Code v2.1.273 之前,超时的 Stop 或 SubagentStop 回调算作失败的 hook 运行,Claude Code 丢弃该事件上你其他 hooks 的决定)。
  • SessionStart:超时的回调算作没有返回输出,会话用你其他 SessionStart hooks 的输出继续。
  • PreModelSwitch:Claude Code 阻止模型切换;不应答的 hook 没有批准切换。
  • 其他事件,如 Notification、PreCompact 和 PostModelSwitch:Claude Code 记录失败并继续。

主会话里的 Stop 或 SessionStart 回调第一次超时时,Claude Code 还会向消息流添加一条 SDKInformationalMessage,说明驱动会话的应用没有响应;只要你的应用保持无响应,之后的超时不会重复该消息。如果你在回调待处理时中断查询,Claude Code 会取消待处理的工具调用(v2.1.208 之前,如果你在待处理的 PreToolUse 回调期间中断,工具调用仍可能继续)。如果你的回调需要更多时间,在它的 HookMatcher 上设更高的 timeout;在 TypeScript 里,用第三个回调参数里的 AbortSignal 在超时触发时优雅地处理取消。

工具被意外阻止

  • 检查所有 PreToolUse hooks 里返回 permissionDecision: 'deny' 的情况
  • 给你的 hooks 加日志,看它们返回什么 permissionDecisionReason
  • 确认匹配器模式没有过宽:空的匹配器匹配所有工具

修改后的输入没有生效

确保 updatedInput 在 hookSpecificOutput 里面,而不是在顶层:

return {
  hookSpecificOutput: {
    hookEventName: "PreToolUse",
    permissionDecision: "allow",
    updatedInput: { command: "new command" }
  }
};

不要把 updatedInput 与 permissionDecision: 'defer' 配对,它会丢弃修改后的输入;省略 permissionDecision 是可以的:修改后的输入仍经正常权限评估生效;你也可以返回 'allow' 自动批准修改后的输入,或 'ask' 把它展示给用户批准。要在 hookSpecificOutput 里包含 hookEventName,标识该输出属于哪种 hook 类型。

Python 里没有会话 hooks

SessionStart 和 SessionEnd 在 TypeScript 里可以注册为 SDK 回调 hooks,但在 Python SDK 里不可用,因为它的 HookEvent 类型省略了它们;在 Python 里,它们只能作为设置文件(如 .claude/settings.json)里定义的 shell 命令 hooks 使用。要从你的 SDK 应用加载 shell 命令 hooks,用 setting_sources 或 settingSources 包含相应的设置来源:

options = ClaudeAgentOptions(
    setting_sources=["project"],  # 加载含 hooks 的 .claude/settings.json
)
const options = {
  settingSources: ["project"] // 加载含 hooks 的 .claude/settings.json
};

要改为在 Python SDK 里把初始化逻辑作为回调来运行,用 client.receive_response() 的第一条消息作为你的触发器。

子智能体的权限提示成倍增加

派生多个子智能体时,每个可能为自己的工具调用单独请求权限。要避免重复提示,用 PreToolUse hooks 自动批准特定工具,或配置权限规则(子智能体继承父对话的权限规则)。

与子智能体的递归 hook 循环

派生子智能体的 UserPromptSubmit hook,如果这些子智能体触发同一个 hook,就可能造成无限循环。要防止:用共享变量或会话状态跟踪你是否已经处在子智能体内;让 hooks 只对顶层智能体会话运行。

systemMessage 没有出现在输出里

systemMessage 字段向用户显示消息,不是向模型。在 Claude Code v2.1.227 及以上,hook 的 systemMessage 可以作为 SDKInformationalMessage 出现在消息流里,是否出现取决于事件(每个事件的章节在 hooks 页上说明输出如何呈现)。要改为向模型传递上下文,返回 additionalContext。v2.1.227 之前,SDK 只对 SessionStart 和 Setup hooks 在消息流里呈现 hook 输出;对任何其他事件,输出只出现在 includeHookEvents(Python 里是 include_hook_events)添加的生命周期事件里,该选项的条目涵盖每个 hook 事件产生哪些生命周期事件。如果你需要可靠地向你的应用呈现 hook 决定,请单独记录它们或用专用的输出通道。

相关资源

  • Claude Code hooks 参考:完整的 JSON 输入/输出 schema、事件文档和匹配器模式
  • Claude Code hooks 指南:shell 命令 hook 示例和演练
  • TypeScript SDK 参考:hook 类型、输入/输出定义和配置选项
  • Python SDK 参考:hook 类型、输入/输出定义和配置选项
  • 权限:控制你的智能体能做什么
  • 自定义工具:构建扩展智能体能力的工具