Bỏ qua để đến nội dung

Kiểm soát hành vi agent bằng hooks

Bài viết được dịch tự động từ bài viết gốc, chưa được kiểm tra lại bởi con người. Chỉ những bài viết có dấu tick xanh cạnh tiêu đề là đã được kiểm tra.

Hooks là các callback function chạy code của bạn để phản ứng với sự kiện agent, như một tool được gọi, một session bắt đầu, hoặc quá trình thực thi dừng lại. Với hooks, bạn có thể:

  • Chặn thao tác nguy hiểm trước khi chúng thực thi, như lệnh shell phá hoại hoặc truy cập file trái phép
  • Log và audit mọi tool call phục vụ compliance, debug, hoặc analytics
  • Biến đổi input và output để sanitize dữ liệu, chèn credential, hoặc redirect file path
  • Yêu cầu con người approve cho các hành động nhạy cảm như ghi database hoặc gọi API
  • Theo dõi vòng đời session để quản lý state, dọn dẹp resource, hoặc gửi notification

Hướng dẫn này bao gồm cách hooks hoạt động và cách cấu hình chúng, kèm ví dụ cho các pattern phổ biến như chặn tool, sửa input, và forward notification.

  1. Một sự kiện xảy ra. Điều gì đó xảy ra trong quá trình thực thi agent và SDK phát ra một event: một tool sắp được gọi (PreToolUse), một tool đã trả về kết quả (PostToolUse), một subagent bắt đầu hoặc dừng, agent đang idle, hoặc quá trình thực thi kết thúc. Xem danh sách đầy đủ event.

  2. SDK thu thập hook đã đăng ký. SDK kiểm tra các hook đã đăng ký cho loại event đó. Bao gồm callback hook bạn truyền trong options.hooks và shell command hook từ settings file khi entry settingSources hoặc setting_sources tương ứng được bật, mặc định là bật với option query() mặc định.

  3. Matcher lọc hook nào chạy. Nếu một hook có pattern matcher (như "Write|Edit"), SDK test nó với target của event (ví dụ, tên tool). Hook không có matcher chạy cho mọi event thuộc loại đó.

  4. Callback function thực thi. Callback function của mỗi hook khớp nhận input về những gì đang xảy ra: tên tool, tham số của nó, session ID, và các chi tiết đặc thù event khác.

  5. Callback của bạn trả về một quyết định. Sau khi thực hiện các thao tác (log, gọi API, validate), callback của bạn trả về một output object báo cho agent biết phải làm gì: allow thao tác, block nó, sửa input, hoặc chèn context vào hội thoại.

Ví dụ sau ghép các bước này lại với nhau. Nó đăng ký một PreToolUse hook (bước 1) với matcher "Write|Edit" (bước 3) để callback chỉ chạy cho tool ghi file. Khi được kích hoạt, callback nhận input của tool (bước 4), kiểm tra xem file path có nhắm tới một file .env không, và trả về permissionDecision: "deny" để chặn thao tác (bước 5):

import asyncio
from claude_agent_sdk import (
AssistantMessage,
ClaudeSDKClient,
ClaudeAgentOptions,
HookMatcher,
ResultMessage,
)
# Define a hook callback that receives tool call details
async def protect_env_files(input_data, tool_use_id, context):
# Extract the file path from the tool's input arguments
file_path = input_data["tool_input"].get("file_path", "")
file_name = file_path.split("/")[-1]
# Block the operation if targeting a .env file
if file_name == ".env":
return {
"hookSpecificOutput": {
"hookEventName": input_data["hook_event_name"],
"permissionDecision": "deny",
"permissionDecisionReason": "Cannot modify .env files",
}
}
# Return empty object to allow the operation
return {}
async def main():
options = ClaudeAgentOptions(
hooks={
# Register the hook for PreToolUse events
# The matcher filters to only Write and Edit tool calls
"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():
# Filter for assistant and result messages
if isinstance(message, (AssistantMessage, ResultMessage)):
print(message)
asyncio.run(main())
import { query, HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";
// Define a hook callback with the HookCallback type
const protectEnvFiles: HookCallback = async (input, toolUseID, { signal }) => {
// Cast input to the specific hook type for type safety
const preInput = input as PreToolUseHookInput;
// Cast tool_input to access its properties (typed as unknown in the SDK)
const toolInput = preInput.tool_input as Record<string, unknown>;
const filePath = toolInput?.file_path as string;
const fileName = filePath?.split("/").pop();
// Block the operation if targeting a .env file
if (fileName === ".env") {
return {
hookSpecificOutput: {
hookEventName: preInput.hook_event_name,
permissionDecision: "deny",
permissionDecisionReason: "Cannot modify .env files"
}
};
}
// Return empty object to allow the operation
return {};
};
for await (const message of query({
prompt: "Create a .env file with the standard local development database configuration",
options: {
hooks: {
// Register the hook for PreToolUse events
// The matcher filters to only Write and Edit tool calls
PreToolUse: [{ matcher: "Write|Edit", hooks: [protectEnvFiles] }]
}
}
})) {
// Filter for assistant and result messages
if (message.type === "assistant" || message.type === "result") {
console.log(message);
}
}

Khi bạn chạy một trong hai script này, Claude thử tạo file .env, hook deny tool call, và phản hồi cuối cùng của Claude giải thích rằng nó không thể tạo file .env.

SDK cung cấp hook cho các giai đoạn khác nhau của quá trình thực thi agent. Một số hook khả dụng ở cả hai SDK, trong khi số khác chỉ có ở TypeScript.

Hook EventPython SDKTypeScript SDKĐiều gì kích hoạt nóVí dụ use case
PreToolUseYêu cầu gọi tool (có thể block hoặc modify)Chặn lệnh shell nguy hiểm
PostToolUseKết quả thực thi toolLog mọi thay đổi file vào audit trail
PostToolUseFailureTool thực thi thất bạiXử lý hoặc log lỗi tool
PostToolBatchKhôngMột batch tool call hoàn tất, một lần mỗi batch trước lời gọi model tiếp theoChèn convention một lần cho cả batch
UserPromptSubmitUser gửi promptChèn context bổ sung vào prompt
UserPromptExpansionKhôngMột lệnh user gõ, hoặc một MCP prompt, mở rộng thành prompt trước khi tới Claude. Không kích hoạt khi Claude tự gọi một skillChặn một lệnh không cho gọi trực tiếp hoặc thêm context khi một skill được gõ
MessageDisplayKhôngMột assistant message có text hoàn tất, một lần mỗi message với toàn bộ textRedact hoặc format lại text hiển thị mà không thay đổi transcript
StopAgent dừng thực thiLưu session state trước khi thoát
StopFailureKhôngTurn kết thúc bằng lỗi API thay vì dừng bình thườngLog lỗi hoặc gửi cảnh báo
SubagentStartSubagent khởi tạoTheo dõi việc spawn task song song
SubagentStopSubagent hoàn thànhGộp kết quả từ các task song song
PreCompactYêu cầu nén hội thoạiLưu trữ toàn bộ transcript trước khi tóm tắt
PostCompactKhôngNén hội thoại hoàn tấtLog bản tóm tắt được tạo ra
PermissionRequestMột tool call cần một quyết định permissionXử lý permission tuỳ biến
PermissionDeniedKhôngAuto mode classifier deny một tool callLog các lần deny của classifier hoặc báo model có thể thử lại
SessionStartKhôngSession khởi tạoKhởi tạo logging và telemetry
SessionEndKhôngSession kết thúcDọn dẹp resource tạm thời
NotificationMessage trạng thái agentGửi cập nhật trạng thái agent tới Slack hoặc PagerDuty
SetupKhôngSetup/bảo trì sessionChạy task khởi tạo
TeammateIdleKhôngTeammate trở nên idlePhân công lại việc hoặc thông báo
TaskCreatedKhôngMột task được tạo qua tool TaskCreateÉp buộc quy ước đặt tên task
TaskCompletedKhôngBackground task hoàn thànhGộp kết quả từ các task song song
ElicitationKhôngMột MCP server yêu cầu input người dùng giữa chừng taskPhản hồi các yêu cầu input MCP theo chương trình
ElicitationResultKhôngMột user phản hồi một MCP elicitationSửa hoặc chặn phản hồi trước khi nó trả về server
ConfigChangeKhôngFile cấu hình thay đổiReload settings động
InstructionsLoadedKhôngMột CLAUDE.md hoặc rules file được load vào contextAudit file instruction nào được load
WorktreeCreateKhôngGit worktree được tạoTheo dõi workspace cô lập
WorktreeRemoveKhôngGit worktree bị xoáDọn dẹp resource workspace
CwdChangedKhôngWorking directory thay đổi trong lúc sessionReload biến môi trường theo từng directory
FileChangedKhôngMột file đang được watch bị sửa, tạo, hoặc xoáReload cấu hình khi file dự án thay đổi
DirectoryAddedKhôngMột working directory được thêm giữa chừng sessionCài dependency cho một repo được thêm giữa chừng session

Để cấu hình một hook, truyền nó trong trường hooks của agent option (ClaudeAgentOptions trong Python, object options trong TypeScript). Đoạn code này giả định bạn đã định nghĩa một hook callback, như protect_env_files trong Python hoặc protectEnvFiles trong TypeScript từ ví dụ ở trên:

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

Option hooks là một dictionary trong Python hoặc một object trong TypeScript, trong đó:

Dùng matcher để lọc khi callback của bạn kích hoạt. Trường matcher khớp với một giá trị khác nhau tuỳ theo loại hook event. Ví dụ, hook dựa trên tool khớp với tên tool, trong khi hook Notification khớp với loại notification. Xem Claude Code hooks reference để biết danh sách đầy đủ giá trị matcher cho mỗi loại event.

Matcher SDK theo cùng quy tắc như matcher trong settings file. Một matcher chỉ chứa chữ cái, số, _, -, khoảng trắng, ,, và | được so sánh như một exact string, với các lựa chọn thay thế cách nhau bởi | hoặc , và khoảng trắng bao quanh tuỳ chọn, nên Write|EditWrite, Edit đều khớp chính xác hai tool đó và code-reviewer chỉ khớp loại agent đó. Một matcher là *, chuỗi rỗng, hoặc bỏ qua matcher hoàn toàn khớp mọi lần xuất hiện của event.

Một matcher chứa bất kỳ ký tự nào khác được đánh giá như một unanchored regular expression, nên ^mcp__ khớp mọi MCP tool và Edit.* khớp cả EditNotebookEdit. Bọc một regular expression trong ^$ khi bạn cần khớp toàn chuỗi.

Một matcher như mcp__memory hoặc mcp__brave-search chỉ chứa ký tự exact-match, nên nó được so sánh như một exact string và không khớp tool nào; dùng mcp__memory__.* để khớp mọi tool từ server đó.

Dấu gạch ngang trong tập exact-match yêu cầu Claude Code runtime v2.1.195 trở lên. Ở phiên bản cũ hơn, một tên có gạch ngang như code-reviewer được đánh giá như một unanchored regular expression và phải được neo lại thành ^code-reviewer$ để khớp chính xác.

StopFailureFileChanged dùng một tập exact-match hẹp hơn chỉ gồm chữ cái, số, _, và |. Một dấu gạch ngang, khoảng trắng, hoặc dấu phẩy trong matcher của hai event này giữ nó trên đường regular-expression, và chỉ | phân tách các lựa chọn thay thế, vậy nên viết rate_limit|overloaded, không phải rate_limit, overloaded. FileChanged còn dùng matcher của nó để xây danh sách watch các tên file literal; xem FileChanged trong hooks reference.

OptionTypeDefaultMô tả
matcherstringundefinedPattern khớp với trường filter của event, theo các quy tắc so sánh ở trên. Với tool hook, đây là tên tool. Built-in tool bao gồm Bash, Read, Write, Edit, Glob, Grep, WebFetch, Agent, và nhiều tool khác (xem Tool Input Types để biết danh sách đầy đủ). MCP tool dùng pattern mcp__<server>__<action>.
hooksHookCallback[]-Bắt buộc. Mảng callback function để thực thi khi pattern khớp
timeoutnumberundefinedTimeout tính bằng giây. Khi bỏ qua, Claude Code áp dụng timeout mặc định của event: 10 phút cho hầu hết event, 30 giây cho UserPromptSubmit. Claude Code dùng giới hạn ngắn hơn cho một số event, như 10 giây cho MessageDisplay và một budget mặc định 1.5 giây cho SessionEnd. Callback SDK của bạn theo mặc định của command hook

Dùng pattern matcher để nhắm vào tool cụ thể bất cứ khi nào có thể. Một matcher với 'Bash' chỉ chạy cho lệnh Bash, trong khi bỏ qua pattern chạy callback của bạn cho mọi lần xuất hiện của event.

Với hook dựa trên tool, matcher chỉ lọc theo tên tool, không theo file path hay tham số khác. Để lọc theo file path, kiểm tra tool_input.file_path bên trong callback của bạn.

Mỗi hook callback nhận ba tham số:

  • Input data: một object có kiểu chứa chi tiết event. Mỗi loại hook có shape input riêng. Ví dụ, PreToolUseHookInput bao gồm tool_nametool_input, trong khi NotificationHookInput bao gồm message. Xem định nghĩa kiểu đầy đủ trong tham khảo SDK TypeScriptPython.
    • Mọi hook input đều chia sẻ session_id, cwd, và hook_event_name.
    • agent_idagent_type được điền khi hook kích hoạt bên trong một subagent. Trong TypeScript, đây là trường trên base hook input và khả dụng cho mọi loại hook. Trong Python, chúng là trường tuỳ chọn trên PreToolUse, PostToolUse, PostToolUseFailure, và PermissionRequest, và trường bắt buộc trên SubagentStartSubagentStop.
  • Tool use ID (str | None / string | undefined): liên kết event PreToolUsePostToolUse cho cùng một tool call.
  • Context: trong TypeScript, chứa một property signal (AbortSignal) để huỷ. Trong Python, tham số này được dành cho tương lai.

Callback của bạn trả về một object với hai nhóm trường:

  • Trường cấp cao nhất hoạt động giống nhau trên mọi event: systemMessage hiển thị một message cho người dùng, và continue (continue_ trong Python) quyết định agent có tiếp tục chạy sau hook này không.
  • hookSpecificOutput kiểm soát thao tác hiện tại. Các trường bên trong tuỳ vào loại hook event. Với hook PreToolUse, đây là nơi bạn đặt permissionDecision ("allow", "deny", "ask", hoặc "defer"), permissionDecisionReason, và updatedInput. Trả về "defer" kết thúc query để bạn có thể resume nó sau. Với hook PostToolUse, bạn có thể đặt additionalContext để bổ sung thông tin vào tool result. Để thay thế output của tool trước khi Claude thấy nó, đặt updatedToolOutput, hoạt động với mọi tool trên cả hai SDK. Trường updatedMCPToolOutput cũ hơn chỉ thay thế output MCP tool và đã deprecated.

Trả về {} để allow thao tác mà không thay đổi gì. Callback hook của SDK dùng cùng format JSON output như shell command hook của Claude Code, tài liệu này mô tả mọi trường và option đặc thù event. Với định nghĩa kiểu SDK, xem tham khảo SDK TypeScriptPython.

Mặc định, agent chờ hook của bạn trả về trước khi tiếp tục. Nếu hook của bạn thực hiện một side effect, như log hoặc gửi webhook, và không cần ảnh hưởng tới hành vi của agent, bạn có thể trả về một output bất đồng bộ thay vào đó. Điều này báo cho agent tiếp tục ngay lập tức mà không chờ hook hoàn tất. Trong đoạn code này, send_to_logging_service trong Python và sendToLoggingService trong TypeScript đại diện cho bất kỳ hàm logging nào bạn định nghĩa:

async def async_hook(input_data, tool_use_id, context):
# Start a background task, then return immediately
asyncio.create_task(send_to_logging_service(input_data))
return {"async_": True, "asyncTimeout": 30000}
const asyncHook: HookCallback = async (input, toolUseID, { signal }) => {
// Start a background task, then return immediately
sendToLoggingService(input).catch(console.error);
return { async: true, asyncTimeout: 30000 };
};
TrườngTypeMô tả
asynctrueBáo hiệu chế độ async. Agent tiếp tục mà không chờ. Trong Python, dùng async_ để tránh từ khoá reserved.
asyncTimeoutnumberTimeout tuỳ chọn tính bằng mili giây cho thao tác background

Một số ví dụ trong phần này chỉ hiển thị callback function. Để chạy một ví dụ, đăng ký callback dưới event tương ứng trong trường hooks của option, như đã mô tả ở Cấu hình hooks.

Ví dụ này chặn Write tool call và viết lại tham số file_path để thêm tiền tố /sandbox, chuyển hướng mọi thao tác ghi file vào một thư mục sandbox. Callback trả về updatedInput với path đã sửa và permissionDecision: 'allow' để tự động approve thao tác đã được viết lại:

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

Để xác nhận việc redirect, đặt tiền tố thành một path bạn có thể ghi được, như ./sandbox hoặc /tmp/sandbox (macOS không cho phép tạo thư mục /sandbox cấp root), sau đó yêu cầu agent ghi một file: kết quả của Write tool trong message stream ghi tên path với tiền tố sandbox của bạn thay vì path Claude đã yêu cầu.

Ví dụ này chặn ghi vào thư mục /etc và giải thích lý do cho cả model lẫn người dùng:

  • permissionDecision: 'deny' dừng tool call.
  • permissionDecisionReason báo cho model biết lý do, để nó tránh thử lại.
  • systemMessage hiển thị cho người dùng biết chuyện gì đã xảy ra.
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 {
# Top-level field: message shown to the user
"systemMessage": "Remember: system directories like /etc are protected.",
# hookSpecificOutput: block the operation
"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 {
// Top-level field: message shown to the user
systemMessage: "Remember: system directories like /etc are protected.",
// hookSpecificOutput: block the operation
hookSpecificOutput: {
hookEventName: preInput.hook_event_name,
permissionDecision: "deny",
permissionDecisionReason: "Writing to /etc is not allowed"
}
};
}
return {};
};

Mặc định, agent có thể prompt xin permission trước khi dùng một số tool. Ví dụ này tự động approve các tool filesystem chỉ đọc (Read, Glob, Grep) bằng cách trả về permissionDecision: 'allow', cho phép chúng chạy mà không cần người dùng xác nhận trong khi các tool khác vẫn chịu permission check bình thường:

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

Khi một event kích hoạt, mọi hook khớp chạy song song. Với quyết định permission, kết quả chặt chẽ nhất được áp dụng: một deny duy nhất chặn tool call bất kể các hook khác trả về gì. Vì thứ tự hoàn thành không xác định, hãy viết mỗi hook hoạt động độc lập thay vì dựa vào việc một hook khác đã chạy trước.

Ví dụ dưới đây đăng ký ba check độc lập cho mọi tool call:

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

Dùng matcher đa tool để chia sẻ một callback cho nhiều tool liên quan. Ví dụ này đăng ký ba matcher với scope khác nhau:

  • Một danh sách exact phân tách bởi dấu gạch đứng (Write|Edit|NotebookEdit) chỉ kích hoạt file_security_hook cho tool sửa file.
  • Một regex (^mcp__) kích hoạt mcp_audit_hook cho bất kỳ MCP tool nào có tên bắt đầu bằng mcp__.
  • Một matcher bị bỏ qua kích hoạt global_logger cho mọi tool call bất kể tên.
options = ClaudeAgentOptions(
hooks={
"PreToolUse": [
# Match file modification tools
HookMatcher(matcher="Write|Edit|NotebookEdit", hooks=[file_security_hook]),
# Match all MCP tools
HookMatcher(matcher="^mcp__", hooks=[mcp_audit_hook]),
# Match everything (no matcher)
HookMatcher(hooks=[global_logger]),
]
}
)
const options = {
hooks: {
PreToolUse: [
// Match file modification tools
{ matcher: "Write|Edit|NotebookEdit", hooks: [fileSecurityHook] },
// Match all MCP tools
{ matcher: "^mcp__", hooks: [mcpAuditHook] },
// Match everything (no matcher)
{ hooks: [globalLogger] }
]
}
};

Dùng hook SubagentStop để theo dõi khi subagent hoàn thành công việc. Xem kiểu input đầy đủ trong tham khảo SDK TypeScriptPython. Ví dụ này log một bản tóm tắt mỗi khi một subagent hoàn tất:

async def subagent_tracker(input_data, tool_use_id, context):
# Log subagent details when it finishes
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 }) => {
// Cast to SubagentStopHookInput to access subagent-specific fields
const subInput = input as SubagentStopHookInput;
// Log subagent details when it finishes
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 có thể thực hiện các thao tác bất đồng bộ như HTTP request. Bắt lỗi bên trong hook của bạn thay vì để nó propagate ra ngoài, vì một exception không được xử lý có thể làm gián đoạn agent.

Ví dụ này gửi một webhook sau khi mỗi tool hoàn tất, log tool nào đã chạy và khi nào. Hook bắt lỗi để một webhook thất bại không làm gián đoạn agent:

import asyncio
import json
import urllib.request
from datetime import datetime
def _send_webhook(tool_name):
"""Synchronous helper that POSTs tool usage data to an external 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):
# Only fire after a tool completes (PostToolUse), not before
if input_data["hook_event_name"] != "PostToolUse":
return {}
try:
# Run the blocking HTTP call in a thread to avoid blocking the event loop
await asyncio.to_thread(_send_webhook, input_data["tool_name"])
except Exception as e:
# Log the error but don't raise. A failed webhook shouldn't stop the agent
print(f"Webhook request failed: {e}")
return {}
import { query, HookCallback, PostToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";
const webhookNotifier: HookCallback = async (input, toolUseID, { signal }) => {
// Only fire after a tool completes (PostToolUse), not before
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()
}),
// Pass signal so the request cancels if the hook times out
signal
});
} catch (error) {
// Handle cancellation separately from other errors
if (error instanceof Error && error.name === "AbortError") {
console.log("Webhook request cancelled");
}
// Don't re-throw. A failed webhook shouldn't stop the agent
}
return {};
};
// Register as a PostToolUse hook
for await (const message of query({
prompt: "Refactor the auth module",
options: {
hooks: {
PostToolUse: [{ hooks: [webhookNotifier] }]
}
}
})) {
console.log(message);
}

Để xác nhận hook kích hoạt, trỏ webhook URL vào một endpoint bạn có thể theo dõi và gửi một prompt dùng tool: hook gửi một POST kèm tên tool và timestamp sau mỗi lần tool hoàn tất.

Dùng hook Notification để nhận system notification từ agent và forward chúng tới dịch vụ bên ngoài. Notification kích hoạt cho các loại event như:

  • permission_prompt khi Claude cần permission
  • idle_prompt khi Claude đang chờ input
  • auth_success khi xác thực hoàn tất
  • elicitation_dialog, elicitation_complete, và elicitation_response cho luồng elicitation của user prompt

Trong headless SDK session, chỉ các event elicitation elicitation_completeelicitation_response kích hoạt hook này; các loại khác được phát ra bởi interactive UI mà SDK session không chạy. Permission request, ví dụ, đi tới canUseTool callback thay vào đó.

Mỗi notification bao gồm một trường message với mô tả dễ đọc và tuỳ chọn một title.

Ví dụ này forward mọi notification tới một kênh Slack. Nó cần một Slack incoming webhook URL, thứ bạn tạo bằng cách thêm một app vào Slack workspace và bật incoming webhook:

import asyncio
import json
import urllib.request
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, HookMatcher
def _send_slack_notification(message):
"""Synchronous helper that sends a message to Slack via incoming webhook."""
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:
# Run the blocking HTTP call in a thread to avoid blocking the event loop
await asyncio.to_thread(_send_slack_notification, input_data.get("message", ""))
except Exception as e:
print(f"Failed to send notification: {e}")
# Return empty object. Notification hooks don't modify agent behavior
return {}
async def main():
options = ClaudeAgentOptions(
hooks={
# Register the hook for Notification events (no matcher needed)
"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";
// Define a hook callback that sends notifications to Slack
const notificationHandler: HookCallback = async (input, toolUseID, { signal }) => {
// Cast to NotificationHookInput to access the message field
const notification = input as NotificationHookInput;
try {
// POST the notification message to a Slack incoming 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}`
}),
// Pass signal so the request cancels if the hook times out
signal
});
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
console.log("Notification cancelled");
} else {
console.error("Failed to send notification:", error);
}
}
// Return empty object. Notification hooks don't modify agent behavior
return {};
};
// Register the hook for Notification events (no matcher needed)
for await (const message of query({
prompt: "Analyze this codebase",
options: {
hooks: {
Notification: [{ hooks: [notificationHandler] }]
}
}
})) {
console.log(message);
}

Khi một event Notification kích hoạt, hook đăng message của notification, kèm tiền tố Agent status:, tới kênh webhook của bạn nhắm tới.

  • Kiểm tra tên hook event đúng và phân biệt hoa thường (PreToolUse, không phải preToolUse)
  • Kiểm tra pattern matcher của bạn khớp chính xác tên tool
  • Đảm bảo hook nằm dưới đúng loại event trong options.hooks
  • Với hook không phải tool hỗ trợ matcher, như NotificationSubagentStop, matcher khớp với trường khác nhau, và Stop bỏ qua matcher hoàn toàn (xem matcher pattern)
  • Hook có thể không kích hoạt khi agent chạm giới hạn max_turns vì session kết thúc trước khi hook có thể thực thi

Matcher chỉ khớp tên tool, không khớp file path hay tham số khác. Để lọc theo file path, kiểm tra tool_input.file_path bên trong hook của bạn:

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 {}; // Skip non-markdown files
// Process markdown files...
return {};
};

Claude Code chạy mỗi callback với một timeout, bạn đặt bằng giây với trường timeout trên HookMatcher của nó. Khi bạn không đặt, Claude Code dùng mặc định của event: 600 giây cho hầu hết event, 30 giây cho UserPromptSubmit, và 10 giây cho MessageDisplay. Claude Code chạy callback SessionEnd trong lúc shutdown dưới SessionEnd timeout budget ngắn hơn.

Khi một callback vượt quá timeout, Claude Code huỷ nó và coi như một hook thất bại: nó bỏ output của callback và session tiếp tục thay vì bị treo. Điều gì xảy ra tiếp theo tuỳ vào event:

  • PreToolUse: Trên v2.1.210 trở lên, Claude Code không chạy tool call, Claude nhận một tool result báo rằng hook không phản hồi trước timeout, và turn tiếp tục. Nếu một hook PreToolUse khác trả về một deny tường minh, Claude nhận deny đó thay vì lỗi timeout. Trước v2.1.210, Claude Code báo timeout cho Claude như một user rejection, khiến session không giám sát dừng lại và chờ input.
  • PostToolUsePostToolUseFailure: Claude Code giữ tool result và turn tiếp tục.
  • UserPromptSubmitUserPromptExpansion: Trên v2.1.208 trở lên, Claude Code chặn prompt kèm một message nêu tên hook và timeout, và session tiếp tục. Vì một callback trên các event này có thể hoạt động như một policy gate, Claude Code không bao giờ để một prompt timeout lọt qua mà không được kiểm tra. Trước v2.1.208, Claude Code kết thúc query bằng error_during_execution khi một callback trên các event này timeout.
  • StopSubagentStop: Claude Code hiển thị một cảnh báo và agent dừng bình thường.
  • Các event khác, như NotificationPreCompact: Claude Code log lỗi và tiếp tục.

Trên v2.1.208 trở lên, nếu bạn ngắt query trong khi một callback đang chờ, Claude Code huỷ tool call đang chờ. Trước v2.1.208, tool call vẫn có thể tiếp tục nếu bạn ngắt trong lúc một callback PreToolUse đang chờ.

Nếu callback của bạn cần thêm thời gian, đặt một timeout cao hơn trên HookMatcher của nó. Trong TypeScript, dùng AbortSignal từ tham số callback thứ ba để xử lý việc huỷ một cách gọn gàng khi timeout kích hoạt.

  • Kiểm tra mọi hook PreToolUse xem có trả về permissionDecision: 'deny' không
  • Thêm logging vào hook của bạn để xem chúng trả về permissionDecisionReason
  • Kiểm tra pattern matcher không quá rộng: một matcher rỗng khớp mọi tool
  • Đảm bảo updatedInput nằm bên trong hookSpecificOutput, không phải ở cấp cao nhất:

    return {
    hookSpecificOutput: {
    hookEventName: "PreToolUse",
    permissionDecision: "allow",
    updatedInput: { command: "new command" }
    }
    };
  • Đừng kết hợp updatedInput với permissionDecision: 'defer', vì nó sẽ bỏ input đã sửa. Bỏ qua permissionDecision thì ổn: input đã sửa vẫn được áp dụng qua đánh giá permission bình thường. Bạn cũng có thể trả về 'allow' để tự động approve input đã sửa hoặc 'ask' để hiển thị nó cho người dùng approve

  • Bao gồm hookEventName trong hookSpecificOutput để xác định output dành cho loại hook nào

SessionStartSessionEnd có thể được đăng ký làm SDK callback hook trong TypeScript, nhưng không khả dụng trong Python SDK vì kiểu HookEvent của nó bỏ qua chúng. Trong Python, chúng chỉ khả dụng dưới dạng shell command hook định nghĩa trong settings file như .claude/settings.json. Để load shell command hook từ ứng dụng SDK của bạn, bao gồm setting source phù hợp với setting_sources hoặc settingSources:

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

Để chạy logic khởi tạo như một Python SDK callback thay vào đó, dùng message đầu tiên từ client.receive_response() làm trigger của bạn.

Khi spawn nhiều subagent, mỗi cái có thể yêu cầu permission riêng biệt. Subagent không tự động kế thừa permission của agent cha. Để tránh prompt lặp lại, dùng hook PreToolUse để tự động approve tool cụ thể, hoặc cấu hình permission rule áp dụng cho subagent session.

Một hook UserPromptSubmit spawn subagent có thể tạo vòng lặp vô hạn nếu các subagent đó kích hoạt cùng hook. Để ngăn điều này:

  • Kiểm tra một chỉ báo subagent trong hook input trước khi spawn
  • Dùng một biến chia sẻ hoặc session state để theo dõi việc bạn có đang ở trong một subagent không
  • Giới hạn hook chỉ chạy cho session agent cấp cao nhất

Trường systemMessage hiển thị một message cho người dùng, không phải model. Mặc định SDK chỉ hiển thị hook output trong message stream cho hook SessionStartSetup, nên một message từ bất kỳ hook event nào khác không xuất hiện trừ khi bạn đặt includeHookEvents (include_hook_events trong Python). Để đưa context tới model thay vào đó, trả về additionalContext.

Nếu bạn cần hiển thị quyết định của hook cho ứng dụng một cách đáng tin cậy, log chúng riêng hoặc dùng một output channel chuyên dụng.

  • Claude Code hooks reference: schema input/output JSON đầy đủ, tài liệu event, và matcher pattern
  • Claude Code hooks guide: ví dụ shell command hook và hướng dẫn thực hành
  • Tham khảo TypeScript SDK: kiểu hook, định nghĩa input/output, và option cấu hình
  • Tham khảo Python SDK: kiểu hook, định nghĩa input/output, và option cấu hình
  • Permissions: kiểm soát những gì agent của bạn có thể làm
  • Custom tools: xây tool để mở rộng khả năng của agent