用 hooks 拦截和控制智能体行为
在 Agent SDK 里用回调函数拦截智能体事件:hooks 的工作方式、全部可用事件(Python/TypeScript 支持情况)、配置与匹配器、回调输入输出、异步输出,以及修改输入、阻止工具、自动批准、多 hook、子智能体跟踪、HTTP 请求、Slack 通知等示例与常见问题。
Hooks 是在智能体事件(如工具被调用、会话启动或执行停止)发生时运行你代码的回调函数。有了 hooks 你可以:
- 阻止危险操作:在它们执行之前,如破坏性的 shell 命令或未授权的文件访问
- 记录和审计:为合规、调试或分析记录每个工具调用
- 转换输入和输出:清洗数据、注入凭据或重定向文件路径
- 要求人工批准:对数据库写入或 API 调用这类敏感动作
- 跟踪会话生命周期:管理状态、清理资源或发送通知
hooks 如何工作
- 事件触发。 智能体执行期间发生了什么事,SDK 就触发一个事件:工具即将被调用(
PreToolUse)、工具返回了结果(PostToolUse)、子智能体启动或停止、智能体空闲,或执行结束(完整事件列表见下)。 - SDK 收集已注册的 hooks。 SDK 检查为该事件类型注册的 hooks,包括你在
options.hooks里传入的回调 hooks,以及设置文件里的 shell 命令 hooks(当对应的settingSources或setting_sources条目启用时,默认的query()选项是启用的)。 - 匹配器过滤哪些 hooks 运行。 如果 hook 有
matcher模式(如"Write|Edit"),SDK 会把它与事件的目标(例如工具名)比较;没有匹配器的 hooks 对该类型的每个事件都运行。 - 回调函数执行。 每个匹配的 hook 的回调函数接收关于发生了什么的输入:工具名、它的参数、会话 ID 和其他事件专属细节。
- 你的回调返回决定。 在执行任何操作(记录、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 SDK | TypeScript 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 匹配器遵循与设置文件里的匹配器相同的规则(那一节记录了精确字符串和正则表达式的评估路径、版本要求以及每种事件类型的匹配器值)。
| 选项 | 类型 | 默认 | 说明 |
|---|---|---|---|
matcher | string | undefined | 与事件的过滤字段匹配的模式,遵循设置文件里匹配器的规则。对工具 hooks,它是工具名:内置工具包括 Bash、Read、Write、Edit、Glob、Grep、WebFetch、Agent 等(完整列表见「工具输入类型」);MCP 工具使用 mcp__<server>__<action> 模式,其中 <server> 是你在 mcpServers 配置里使用的键 |
hooks | HookCallback[] | - | 必填。模式匹配时要执行的回调函数数组 |
timeout | number | undefined | 超时秒数;省略时 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 事件类型:对PreToolUsehooks,这里设permissionDecision("allow"、"deny"、"ask"或"defer")、permissionDecisionReason和updatedInput(返回"defer"会结束查询,以便你之后恢复它);对PostToolUsehooks,可以设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 };
};| 字段 | 类型 | 说明 |
|---|---|---|
async | true | 表示异步模式,智能体不等待就继续;在 Python 里用 async_ 以避开保留字 |
asyncTimeout | number | 后台操作的可选超时毫秒数 |
异步输出无法阻止、修改或向操作注入上下文,因为智能体已经继续了;只把它们用于记录、指标或通知这类副作用。
示例
本节的几个示例只显示回调函数。要运行其中之一,把回调注册在选项 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 在超时前没有响应的工具结果,轮次继续;如果另一个PreToolUsehook 返回了显式 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:超时的回调算作没有返回输出,会话用你其他SessionStarthooks 的输出继续。PreModelSwitch:Claude Code 阻止模型切换;不应答的 hook 没有批准切换。- 其他事件,如
Notification、PreCompact和PostModelSwitch:Claude Code 记录失败并继续。
主会话里的 Stop 或 SessionStart 回调第一次超时时,Claude Code 还会向消息流添加一条 SDKInformationalMessage,说明驱动会话的应用没有响应;只要你的应用保持无响应,之后的超时不会重复该消息。如果你在回调待处理时中断查询,Claude Code 会取消待处理的工具调用(v2.1.208 之前,如果你在待处理的 PreToolUse 回调期间中断,工具调用仍可能继续)。如果你的回调需要更多时间,在它的 HookMatcher 上设更高的 timeout;在 TypeScript 里,用第三个回调参数里的 AbortSignal 在超时触发时优雅地处理取消。
工具被意外阻止
- 检查所有
PreToolUsehooks 里返回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 类型、输入/输出定义和配置选项
- 权限:控制你的智能体能做什么
- 自定义工具:构建扩展智能体能力的工具