Claude Code Agent SDK 钩子(Hooks)允许你在工具调用前后、会话生命周期等关键节点执行自定义逻辑,用于阻止危险操作、审计日志、转换输入输出或要求人工审批。配置时需在 options.hooks 中注册事件类型的 matcher 和回调函数,回调返回特定 JSON 输出以控制行为。常见问题包括钩子未触发、matcher 不生效、输入修改未应用等,可通过检查事件名大小写、matcher 匹配工具名、确保 updatedInput 位于 hookSpecificOutput 内等方法排查。

Claude Code Agent SDK 钩子配置与排查

在关键执行点拦截并自定义代理行为。

钩子是回调函数,用于响应代理事件(如工具被调用、会话开始、执行停止)时运行你的代码。通过钩子,你可以:

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

本指南介绍钩子工作原理、配置方法以及常见模式示例(如阻止工具、修改输入、转发通知)。

钩子工作原理

  1. 代理执行过程中发生某件事,SDK 触发一个事件:即将调用工具(PreToolUse)、工具返回结果(PostToolUse)、子代理启动或停止、代理空闲或执行结束。参见可用钩子事件
  2. SDK 检查针对该事件类型注册的钩子。这包括你在 options.hooks 中传入的回调钩子,以及设置文件中的 shell 命令钩子(当相应的 settingSourcessetting_sources 条目启用时,默认 query() 选项会启用它们)。
  3. 如果钩子包含 matcher 模式(例如 "Write|Edit"),SDK 会将其与事件的目标(例如工具名称)进行匹配。没有 matcher 的钩子会为该类型的每个事件运行。
  4. 每个匹配钩子的回调函数接收关于当前发生事件的信息:工具名称、参数、会话 ID 和其他特定事件细节。
  5. 执行任何操作(日志记录、API 调用、验证)后,你的回调返回一个输出对象,指示代理要做什么:允许操作、阻止操作、修改输入或向对话注入上下文。

以下示例将这些步骤放在一起。它注册了一个 PreToolUse 钩子(步骤 1)并带有 "Write|Edit" matcher(步骤 3),因此回调仅在写入文件的工具上触发。触发时,回调接收工具的输入(步骤 4),检查文件路径是否指向 .env 文件,并返回 permissionDecision: "deny" 来阻止操作(步骤 5):

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

# 定义一个接收工具调用详细信息的钩子回调
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 事件注册钩子
            # matcher 过滤仅针对 Write 和 Edit 工具调用
            "PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[protect_env_files])]
        }
    )

    async with ClaudeSDKClient(options=options) as client:
        await client.query("Update the database configuration")
        async for message in client.receive_response():
            # 过滤出 assistant 和 result 消息
            if isinstance(message, (AssistantMessage, ResultMessage)):
                print(message)

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

// 使用 HookCallback 类型定义钩子回调
const protectEnvFiles: HookCallback = async (input, toolUseID, { signal }) => {
  // 将 input 强制转换为特定的钩子类型以保证类型安全
  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: "Update the database configuration",
  options: {
    hooks: {
      // 为 PreToolUse 事件注册钩子
      // matcher 过滤仅针对 Write 和 Edit 工具调用
      PreToolUse: [{ matcher: "Write|Edit", hooks: [protectEnvFiles] }]
    }
  }
})) {
  // 过滤出 assistant 和 result 消息
  if (message.type === "assistant" || message.type === "result") {
    console.log(message);
  }
}

可用钩子事件

SDK 提供不同代理执行阶段的钩子。部分钩子在两个 SDK 中均可用,另一些仅限 TypeScript。

钩子事件 Python SDK TypeScript SDK 触发时机 示例用例
PreToolUse 工具调用请求(可阻止或修改) 阻止危险 shell 命令
PostToolUse 工具执行结果 将所有文件更改记录到审计日志
PostToolUseFailure 工具执行失败 处理或记录工具错误
PostToolBatch 整批工具调用解析,在下次模型调用前每个批次触发一次 为整个批次一次性注入约定
UserPromptSubmit 用户提示提交 向提示注入额外上下文
Stop 代理执行停止 在退出前保存会话状态
SubagentStart 子代理初始化 跟踪并行任务启动
SubagentStop 子代理完成 聚合来自并行任务的结果
PreCompact 对话压缩请求 在总结前归档完整对话记录
PermissionRequest 权限对话框即将显示 自定义权限处理
SessionStart 会话初始化 初始化日志记录和遥测
SessionEnd 会话终止 清理临时资源
Notification 代理状态消息 向 Slack 或 PagerDuty 发送代理状态更新
Setup 会话设置/维护 运行初始化任务
TeammateIdle 队友变为空闲 重新分配工作或通知
TaskCompleted 后台任务完成 聚合来自并行任务的结果
ConfigChange 配置文件更改 动态重新加载设置
WorktreeCreate Git 工作树创建 跟踪隔离工作区
WorktreeRemove Git 工作树移除 清理工作区资源

配置钩子

在代理选项(Python 中的 ClaudeAgentOptions,TypeScript 中的 options 对象)的 hooks 字段中传入钩子:

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),其中:

Matchers

使用 matcher 过滤回调的触发时机。matcher 字段是一个正则表达式字符串,根据钩子事件类型匹配不同的目标值。例如,工具钩子匹配工具名称,而 Notification 钩子匹配通知类型。参见 Claude Code 钩子参考中每个事件类型的完整 matcher 值列表。

选项 类型 默认值 描述
matcher string undefined 正则表达式模式,匹配事件的目标字段。对于工具钩子,是工具名称。内置工具包括 BashReadWriteEditGlobGrepWebFetchAgent 等(参见 Tool Input Types 获取完整列表)。MCP 工具使用模式 mcp__<server>__<action>
hooks HookCallback[] - 必填。匹配模式时执行的回调函数数组
timeout number 60 超时时间(秒)

尽可能使用 matcher 模式来针对特定工具。使用 'Bash' 的 matcher 仅对 Bash 命令运行,而省略模式则会使回调对该事件的每次出现都运行。注意,对于工具钩子,matcher 仅按工具名称过滤,而非按文件路径或其他参数。若需按文件路径过滤,请在回调内部检查 tool_input.file_path

发现工具名称: 参见 Tool Input Types 获取内置工具完整列表,或添加一个不带 matcher 的钩子来记录会话中所有工具调用。

MCP 工具命名: MCP 工具始终以 mcp__ 开头,后接服务器名称和操作:mcp__<server>__<action>。例如,如果你配置了一个名为 playwright 的服务器,其工具将被命名为 mcp__playwright__browser_screenshotmcp__playwright__browser_click 等。服务器名称来自你在 mcpServers 配置中使用的键。

回调函数

输入

每个钩子回调接收三个参数:

  • 输入数据: 包含事件详细信息的类型化对象。每种钩子类型都有其自己的输入形状(例如,PreToolUseHookInput 包含 tool_nametool_input,而 NotificationHookInput 包含 message)。参见 TypeScriptPython SDK 参考中的完整类型定义。
    • 所有钩子输入共享 session_idcwdhook_event_name
    • 当钩子在子代理内触发时,agent_idagent_type 会被填充。在 TypeScript 中,它们位于基本钩子输入上,对所有钩子类型可用。在 Python 中,它们仅适用于 PreToolUsePostToolUsePostToolUseFailure
  • 工具使用 IDstr | None / string | undefined):关联同一工具调用的 PreToolUsePostToolUse 事件。
  • 上下文: 在 TypeScript 中,包含一个 signal 属性(AbortSignal)用于取消。在 Python 中,此参数保留供将来使用。

输出

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

  • 顶层字段 在每个事件上作用相同:systemMessage 向用户显示一条消息,continue(Python 中为 continue_)决定此钩子后代理是否继续运行。
  • hookSpecificOutput 控制当前操作。内部字段取决于钩子事件类型。对于 PreToolUse 钩子,此处设置 permissionDecision"allow""deny""ask""defer")、permissionDecisionReasonupdatedInput。返回 "defer" 会结束查询,以便你稍后恢复它。对于 PostToolUse 钩子,可以设置 additionalContext 向工具结果追加信息,或者设置 updatedToolOutput 在 Claude 看到之前完全替换工具的输出。

返回 {} 以允许操作保持不变。SDK 回调钩子使用与 Claude Code shell 命令钩子相同的 JSON 输出格式,该格式记录了每个字段和事件特定选项。有关 SDK 类型定义,请参见 TypeScriptPython SDK 参考。

当存在多个钩子或权限规则时,deny 优先于 deferdefer 优先于 askask 优先于 allow。如果任何钩子返回 deny,则无论其他钩子如何,操作都会被阻止。

异步输出

默认情况下,代理会等待你的钩子返回后再继续。如果你的钩子执行副作用(日志记录、发送 webhook)且不需要影响代理行为,你可以返回异步输出。这告诉代理立即继续,无需等待钩子完成:

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 };
};
字段 类型 描述
async true 标记异步模式。代理继续运行,不等待。在 Python 中使用 async_ 以避免保留关键字。
asyncTimeout number 后台操作的可选超时时间(毫秒)

异步输出无法阻止、修改或向当前操作注入上下文,因为代理已经继续执行。仅将它们用于日志记录、指标或通知等副作用。

示例

修改工具输入

此示例拦截 Write 工具调用,重写 file_path 参数以添加 /sandbox 前缀,将所有文件写入重定向到沙盒目录。回调返回带有修改后路径的 updatedInputpermissionDecision: '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' 以向用户显示。使用 'defer' 时,updatedInput 会被忽略。始终返回一个新的对象,而不是修改原始的 tool_input

添加上下文并阻止工具

此示例阻止写入 /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 {};
};

自动批准特定工具

默认情况下,代理可能会在使用某些工具前提示权限。此示例通过返回 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 {};
};

注册多个钩子

当一个事件触发时,所有匹配的钩子并行运行。对于权限决策,最严格的结果胜出:单个 deny 会阻止工具调用,无论其他钩子返回什么。由于完成顺序是不确定的,请将每个钩子编写为独立运行,而不要依赖另一个钩子已先运行。

以下示例为每次工具调用注册三个独立的检查:

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] }
    ]
  }
};

使用正则表达式 matcher 过滤

使用正则模式匹配多个工具。此示例注册了三个不同范围的 matcher:第一个仅针对文件修改工具触发 file_security_hook,第二个针对任何 MCP 工具(名称以 mcp__ 开头的工具)触发 mcp_audit_hook,第三个针对每次工具调用(无论名称如何)触发 global_logger

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

跟踪子代理活动

使用 SubagentStop 钩子监控子代理完成工作的情况。参见 TypeScriptPython 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] }]
  }
};

从钩子发出 HTTP 请求

钩子可以执行异步操作,如 HTTP 请求。请在钩子内部捕获错误,而不要让它们传播,因为未处理的异常可能会中断代理。

此示例在每次工具完成后发送一个 webhook,记录运行了哪个工具以及何时运行。钩子会捕获错误,因此失败的 webhook 不会中断代理:

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

def _send_webhook(tool_name):
    """同步辅助函数:向外部 webhook 发送工具使用数据的 POST 请求。"""
    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:
        # 记录错误但不抛出。失败的 webhook 不应停止代理
        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,以便在钩子超时时取消请求
      signal
    });
  } catch (error) {
    // 将取消与其他错误分开处理
    if (error instanceof Error && error.name === "AbortError") {
      console.log("Webhook request cancelled");
    }
    // 不要重新抛出。失败的 webhook 不应停止代理
  }

  return {};
};

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

转发通知到 Slack

使用 Notification 钩子接收来自代理的系统通知并将其转发到外部服务。通知针对特定事件类型触发:permission_prompt(Claude 需要权限)、idle_prompt(Claude 正在等待输入)、auth_success(认证完成)、elicitation_dialog(Claude 正在提示用户)、elicitation_response(用户回答了提示)、elicitation_complete(提示结束)。每条通知包含一个包含人类可读描述的 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 钩子不修改代理行为
    return {}

async def main():
    options = ClaudeAgentOptions(
        hooks={
            # 为 Notification 事件注册钩子(无需 matcher)
            "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 发送通知的钩子回调
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,以便在钩子超时时取消请求
      signal
    });
  } catch (error) {
    if (error instanceof Error && error.name === "AbortError") {
      console.log("Notification cancelled");
    } else {
      console.error("Failed to send notification:", error);
    }
  }

  // 返回空对象。Notification 钩子不修改代理行为
  return {};
};

// 为 Notification 事件注册钩子(无需 matcher)
for await (const message of query({
  prompt: "Analyze this codebase",
  options: {
    hooks: {
      Notification: [{ hooks: [notificationHandler] }]
    }
  }
})) {
  console.log(message);
}

常见问题排查

钩子不触发

  • 确认钩子事件名称正确且区分大小写(PreToolUse 而非 preToolUse)。
  • 检查 matcher 模式是否与工具名称精确匹配。
  • 确保钩子在 options.hooks 中位于正确的事件类型下。
  • 对于非工具钩子(如 StopSubagentStop),matcher 匹配的是不同字段(参见 matcher patterns)。
  • 当代理达到 max_turns 限制时,钩子可能不会触发,因为会话在钩子执行前已结束。

Matcher 过滤不符合预期

Matcher 仅匹配工具名称,而非文件路径或其他参数。若需按文件路径过滤,请在钩子内部检查 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 {};
};

钩子超时

  • 增大 HookMatcher 配置中的 timeout 值。
  • 在 TypeScript 中,使用第三个回调参数中的 AbortSignal 以优雅处理取消。

工具被意外阻止

  • 检查所有 PreToolUse 钩子是否返回了 permissionDecision: 'deny'
  • 在钩子中添加日志,查看它们返回的 permissionDecisionReason
  • 确认 matcher 模式没有过于宽泛(空的 matcher 会匹配所有工具)。

修改的输入未生效

  • 确保 updatedInput 位于 hookSpecificOutput 内部,而非顶层:

    return {
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "allow",
        updatedInput: { command: "new command" }
      }
    };
  • 你必须同时返回 permissionDecision: 'allow''ask' 以使输入修改生效。

  • hookSpecificOutput 中包含 hookEventName,以标识输出针对哪种钩子类型。

Python 中缺少会话钩子

SessionStartSessionEnd 可以作为 TypeScript 中的 SDK 回调钩子注册,但在 Python SDK 中不可用(HookEvent 排除了它们)。在 Python 中,它们仅作为设置文件中定义的 shell 命令钩子可用(例如 .claude/settings.json)。要从 SDK 应用程序加载 shell 命令钩子,请使用适当的设置源 setting_sourcessettingSources

options = ClaudeAgentOptions(
    setting_sources=["project"],  # 加载 .claude/settings.json,包括钩子
)
const options = {
  settingSources: ["project"] // 加载 .claude/settings.json,包括钩子
};

如果希望以 Python SDK 回调方式运行初始化逻辑,可以将 client.receive_response() 的第一条消息作为触发器。

子代理权限提示倍增

当生成多个子代理时,每个子代理可能单独请求权限。子代理不会自动继承父代理的权限。为避免重复提示,使用 PreToolUse 钩子自动批准特定工具,或者配置适用于子代理会话的权限规则。

子代理导致的递归钩子循环

UserPromptSubmit 钩子如果生成了子代理,而这些子代理又触发相同的钩子,则可能产生无限循环。为防止这种情况:

  • 在生成子代理之前,检查钩子输入中是否存在子代理指示符。
  • 使用共享变量或会话状态跟踪当前是否已处于子代理内部。
  • 将钩子限定为仅在顶层代理会话中运行。

systemMessage 未出现在输出中

systemMessage 字段显示一条消息给用户,而非模型。默认情况下,SDK 不会在消息流中显示钩子输出,因此除非你设置了 includeHookEvents(Python 中为 include_hook_events),否则消息可能不会出现。若要将上下文传递给模型,请返回 additionalContext

如果需要可靠地将钩子决策传递给应用程序,请单独记录它们或使用专用的输出通道。

相关资源

常见问题

钩子不触发是什么原因?

检查事件名称大小写是否准确(例如 PreToolUse 而非 preToolUse),matcher 模式是否匹配工具名称,以及钩子是否在 options.hooks 中位于正确的事件类型下。另外,max_turns 限制可能导致钩子在执行前会话终止。

matcher 对工具路径不生效怎么解决?

Matcher 仅匹配工具名称,不匹配文件路径或参数。若需按文件路径过滤,请在回调内部检查 tool_input.file_path 字段,例如 if (!filePath?.endsWith(".md")) return {};

修改了输入但没生效,为什么?

确保 updatedInput 位于 hookSpecificOutput 内部,而非顶层返回对象中。同时必须返回 permissionDecision: 'allow''ask' 才能使输入修改生效。另外,hookSpecificOutput 中需要包含 hookEventName 字段。