Appearance
Claude Code Agent SDK 钩子(Hooks)允许你在工具调用前后、会话生命周期等关键节点执行自定义逻辑,用于阻止危险操作、审计日志、转换输入输出或要求人工审批。配置时需在 options.hooks 中注册事件类型的 matcher 和回调函数,回调返回特定 JSON 输出以控制行为。常见问题包括钩子未触发、matcher 不生效、输入修改未应用等,可通过检查事件名大小写、matcher 匹配工具名、确保 updatedInput 位于 hookSpecificOutput 内等方法排查。
Claude Code Agent SDK 钩子配置与排查
在关键执行点拦截并自定义代理行为。
钩子是回调函数,用于响应代理事件(如工具被调用、会话开始、执行停止)时运行你的代码。通过钩子,你可以:
- 在危险操作执行前阻止,例如破坏性 shell 命令或未授权的文件访问。
- 记录和审计每次工具调用,用于合规、调试或分析。
- 转换输入和输出,用于清理数据、注入凭据或重定向文件路径。
- 要求人工审批敏感操作,如数据库写入或 API 调用。
- 跟踪会话生命周期,以管理状态、清理资源或发送通知。
本指南介绍钩子工作原理、配置方法以及常见模式示例(如阻止工具、修改输入、转发通知)。
钩子工作原理
- 代理执行过程中发生某件事,SDK 触发一个事件:即将调用工具(
PreToolUse)、工具返回结果(PostToolUse)、子代理启动或停止、代理空闲或执行结束。参见可用钩子事件。 - SDK 检查针对该事件类型注册的钩子。这包括你在
options.hooks中传入的回调钩子,以及设置文件中的 shell 命令钩子(当相应的settingSources或setting_sources条目启用时,默认query()选项会启用它们)。 - 如果钩子包含
matcher模式(例如"Write|Edit"),SDK 会将其与事件的目标(例如工具名称)进行匹配。没有 matcher 的钩子会为该类型的每个事件运行。 - 每个匹配钩子的回调函数接收关于当前发生事件的信息:工具名称、参数、会话 ID 和其他特定事件细节。
- 执行任何操作(日志记录、API 调用、验证)后,你的回调返回一个输出对象,指示代理要做什么:允许操作、阻止操作、修改输入或向对话注入上下文。
以下示例将这些步骤放在一起。它注册了一个 PreToolUse 钩子(步骤 1)并带有 "Write|Edit" matcher(步骤 3),因此回调仅在写入文件的工具上触发。触发时,回调接收工具的输入(步骤 4),检查文件路径是否指向 .env 文件,并返回 permissionDecision: "deny" 来阻止操作(步骤 5):
python
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())typescript
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 字段中传入钩子:
python
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)typescript
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 | 正则表达式模式,匹配事件的目标字段。对于工具钩子,是工具名称。内置工具包括 Bash、Read、Write、Edit、Glob、Grep、WebFetch、Agent 等(参见 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_screenshot、mcp__playwright__browser_click等。服务器名称来自你在mcpServers配置中使用的键。
回调函数
输入
每个钩子回调接收三个参数:
- 输入数据: 包含事件详细信息的类型化对象。每种钩子类型都有其自己的输入形状(例如,
PreToolUseHookInput包含tool_name和tool_input,而NotificationHookInput包含message)。参见 TypeScript 和 Python SDK 参考中的完整类型定义。- 所有钩子输入共享
session_id、cwd和hook_event_name。 - 当钩子在子代理内触发时,
agent_id和agent_type会被填充。在 TypeScript 中,它们位于基本钩子输入上,对所有钩子类型可用。在 Python 中,它们仅适用于PreToolUse、PostToolUse和PostToolUseFailure。
- 所有钩子输入共享
- 工具使用 ID(
str | None/string | undefined):关联同一工具调用的PreToolUse和PostToolUse事件。 - 上下文: 在 TypeScript 中,包含一个
signal属性(AbortSignal)用于取消。在 Python 中,此参数保留供将来使用。
输出
你的回调返回一个包含两类字段的对象:
- 顶层字段 在每个事件上作用相同:
systemMessage向用户显示一条消息,continue(Python 中为continue_)决定此钩子后代理是否继续运行。 hookSpecificOutput控制当前操作。内部字段取决于钩子事件类型。对于PreToolUse钩子,此处设置permissionDecision("allow"、"deny"、"ask"或"defer")、permissionDecisionReason和updatedInput。返回"defer"会结束查询,以便你稍后恢复它。对于PostToolUse钩子,可以设置additionalContext向工具结果追加信息,或者设置updatedToolOutput在 Claude 看到之前完全替换工具的输出。
返回 {} 以允许操作保持不变。SDK 回调钩子使用与 Claude Code shell 命令钩子相同的 JSON 输出格式,该格式记录了每个字段和事件特定选项。有关 SDK 类型定义,请参见 TypeScript 和 Python SDK 参考。
当存在多个钩子或权限规则时,deny 优先于 defer,defer 优先于 ask,ask 优先于 allow。如果任何钩子返回
deny,则无论其他钩子如何,操作都会被阻止。
异步输出
默认情况下,代理会等待你的钩子返回后再继续。如果你的钩子执行副作用(日志记录、发送 webhook)且不需要影响代理行为,你可以返回异步输出。这告诉代理立即继续,无需等待钩子完成:
python
async def async_hook(input_data, tool_use_id, context):
# 启动后台任务,然后立即返回
asyncio.create_task(send_to_logging_service(input_data))
return {"async_": True, "asyncTimeout": 30000}typescript
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 前缀,将所有文件写入重定向到沙盒目录。回调返回带有修改后路径的 updatedInput 和 permissionDecision: 'allow',以自动批准重写后的操作:
python
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 {}typescript
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向用户显示发生了什么。
python
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 {}typescript
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),让它们无需用户确认即可运行,同时所有其他工具仍受正常权限检查:
python
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 {}typescript
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 会阻止工具调用,无论其他钩子返回什么。由于完成顺序是不确定的,请将每个钩子编写为独立运行,而不要依赖另一个钩子已先运行。
以下示例为每次工具调用注册三个独立的检查:
python
options = ClaudeAgentOptions(
hooks={
"PreToolUse": [
HookMatcher(hooks=[authorization_check]),
HookMatcher(hooks=[input_validator]),
HookMatcher(hooks=[audit_logger]),
]
}
)typescript
const options = {
hooks: {
PreToolUse: [
{ hooks: [authorizationCheck] },
{ hooks: [inputValidator] },
{ hooks: [auditLogger] }
]
}
};使用正则表达式 matcher 过滤
使用正则模式匹配多个工具。此示例注册了三个不同范围的 matcher:第一个仅针对文件修改工具触发 file_security_hook,第二个针对任何 MCP 工具(名称以 mcp__ 开头的工具)触发 mcp_audit_hook,第三个针对每次工具调用(无论名称如何)触发 global_logger:
python
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]),
]
}
)typescript
const options = {
hooks: {
PreToolUse: [
// 匹配文件修改工具
{ matcher: "Write|Edit|Delete", hooks: [fileSecurityHook] },
// 匹配所有 MCP 工具
{ matcher: "^mcp__", hooks: [mcpAuditHook] },
// 匹配所有(无 matcher)
{ hooks: [globalLogger] }
]
}
};跟踪子代理活动
使用 SubagentStop 钩子监控子代理完成工作的情况。参见 TypeScript 和 Python SDK 参考中的完整输入类型。以下示例在子代理每次完成时记录摘要:
python
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])]}
)typescript
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 不会中断代理:
python
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 {}typescript
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 来创建。
python
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())typescript
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中位于正确的事件类型下。 - 对于非工具钩子(如
Stop和SubagentStop),matcher 匹配的是不同字段(参见 matcher patterns)。 - 当代理达到
max_turns限制时,钩子可能不会触发,因为会话在钩子执行前已结束。
Matcher 过滤不符合预期
Matcher 仅匹配工具名称,而非文件路径或其他参数。若需按文件路径过滤,请在钩子内部检查 tool_input.file_path:
typescript
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内部,而非顶层:typescriptreturn { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { command: "new command" } } };你必须同时返回
permissionDecision: 'allow'或'ask'以使输入修改生效。在
hookSpecificOutput中包含hookEventName,以标识输出针对哪种钩子类型。
Python 中缺少会话钩子
SessionStart 和 SessionEnd 可以作为 TypeScript 中的 SDK 回调钩子注册,但在 Python SDK 中不可用(HookEvent 排除了它们)。在 Python 中,它们仅作为设置文件中定义的 shell 命令钩子可用(例如 .claude/settings.json)。要从 SDK 应用程序加载 shell 命令钩子,请使用适当的设置源 setting_sources 或 settingSources:
python
options = ClaudeAgentOptions(
setting_sources=["project"], # 加载 .claude/settings.json,包括钩子
)typescript
const options = {
settingSources: ["project"] // 加载 .claude/settings.json,包括钩子
};如果希望以 Python SDK 回调方式运行初始化逻辑,可以将 client.receive_response() 的第一条消息作为触发器。
子代理权限提示倍增
当生成多个子代理时,每个子代理可能单独请求权限。子代理不会自动继承父代理的权限。为避免重复提示,使用 PreToolUse 钩子自动批准特定工具,或者配置适用于子代理会话的权限规则。
子代理导致的递归钩子循环
UserPromptSubmit 钩子如果生成了子代理,而这些子代理又触发相同的钩子,则可能产生无限循环。为防止这种情况:
- 在生成子代理之前,检查钩子输入中是否存在子代理指示符。
- 使用共享变量或会话状态跟踪当前是否已处于子代理内部。
- 将钩子限定为仅在顶层代理会话中运行。
systemMessage 未出现在输出中
systemMessage 字段显示一条消息给用户,而非模型。默认情况下,SDK 不会在消息流中显示钩子输出,因此除非你设置了 includeHookEvents(Python 中为 include_hook_events),否则消息可能不会出现。若要将上下文传递给模型,请返回 additionalContext。
如果需要可靠地将钩子决策传递给应用程序,请单独记录它们或使用专用的输出通道。
相关资源
- Claude Code 钩子参考:完整的 JSON 输入/输出模式、事件文档和 matcher 模式。
- Claude Code 钩子指南:shell 命令钩子示例和演练。
- TypeScript SDK 参考:钩子类型、输入/输出定义和配置选项。
- Python SDK 参考:钩子类型、输入/输出定义和配置选项。
- 权限:控制你的代理可以做什么。
- 自定义工具:构建工具以扩展代理能力。
常见问题
钩子不触发是什么原因?
检查事件名称大小写是否准确(例如 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 字段。