رفتن به محتوا

رهگیری و کنترلِ رفتارِ ایجنت با hookها

Hookها توابعِ callback هستند که کدِ تو را در پاسخ به رویدادهای ایجنت اجرا می‌کنند، مثلِ فراخوانی‌شدنِ یک ابزار، شروعِ یک نشست، یا توقفِ اجرا. با hookها می‌توانی:

  • عملیاتِ خطرناک را مسدود کنی پیش از اجرایشان، مثلِ دستورهای مخربِ shell یا دسترسیِ غیرمجاز به فایل
  • هر فراخوانیِ ابزار را لاگ و حسابرسی کنی برای انطباق، دیباگ، یا تحلیل
  • ورودی‌ها و خروجی‌ها را دگرگون کنی تا داده‌ها را پاک‌سازی کنی، اعتبارنامه تزریق کنی، یا مسیرهای فایل را تغییرِ مسیر دهی
  • برای اقداماتِ حساس تأییدِ انسانی بخواهی مثلِ نوشتن در دیتابیس یا فراخوانیِ API
  • چرخه‌ی عمرِ نشست را ردگیری کنی تا state را مدیریت کنی، منابع را پاک کنی، یا اعلان بفرستی

این راهنما پوشش می‌دهد که hookها چطور کار می‌کنند، چطور پیکربندی‌شان کنی، و مثال‌هایی برای الگوهای رایج مثلِ مسدودکردنِ ابزار، تغییرِ ورودی‌ها، و فورواردکردنِ اعلان‌ها فراهم می‌کند.

یک رویداد شلیک می‌شود

چیزی در طولِ اجرای ایجنت اتفاق می‌افتد و SDK یک رویداد شلیک می‌کند: ابزاری در شُرفِ فراخوانی است (PreToolUse)، ابزاری نتیجه‌ای برگردانده (PostToolUse)، یک ساب‌ایجنت شروع یا متوقف شده، ایجنت بیکار است، یا اجرا تمام شده. به فهرستِ کاملِ رویدادها نگاه کن.

SDK hookهای ثبت‌شده را جمع می‌کند

SDK hookهای ثبت‌شده برای آن نوعِ رویداد را بررسی می‌کند. این شاملِ callback hookهایی است که در options.hooks پاس می‌دهی و shell command hookها از فایل‌های تنظیمات وقتی ورودیِ متناظرِ settingSources یا setting_sources فعال باشد، که برای options‌های پیش‌فرضِ query() فعال است.

Matcherها فیلتر می‌کنند که کدام hookها اجرا شوند

اگر یک hook یک الگوی matcher داشته باشد (مثلِ "Write|Edit")، SDK آن را در برابرِ هدفِ رویداد (مثلاً نامِ ابزار) آزمایش می‌کند. hookهای بدونِ matcher برای هر رویداد از آن نوع اجرا می‌شوند.

توابعِ callback اجرا می‌شوند

تابعِ callbackِ هر hookِ منطبق ورودی‌ای درباره‌ی آنچه در حالِ رخ‌دادن است دریافت می‌کند: نامِ ابزار، آرگومان‌هایش، session ID، و دیگر جزئیاتِ خاصِ رویداد.

callbackِ تو یک تصمیم برمی‌گرداند

پس از انجامِ هر عملیات (لاگ، فراخوانیِ API، اعتبارسنجی)، callbackِ تو یک شیءِ خروجی برمی‌گرداند که به ایجنت می‌گوید چه کند: عملیات را اجازه بده، مسدودش کن، ورودی را تغییر بده، یا کانتکست را به گفتگو تزریق کن.

مثالِ زیر این گام‌ها را کنارِ هم می‌گذارد. یک hookِ PreToolUse (گامِ ۱) را با یک matcherِ "Write|Edit" (گامِ ۳) ثبت می‌کند تا callback فقط برای ابزارهای نوشتنِ فایل شلیک شود. وقتی فعال می‌شود، callback ورودیِ ابزار را دریافت می‌کند (گامِ ۴)، بررسی می‌کند آیا مسیرِ فایل یک فایلِ .env را هدف گرفته، و permissionDecision: "deny" برمی‌گرداند تا عملیات را مسدود کند (گامِ ۵):

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("Update the 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: "Update the 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);
}
}

SDK برای مراحلِ مختلفِ اجرای ایجنت hook فراهم می‌کند. برخی hookها در هر دو SDK موجودند، در حالی که برخی فقط در TypeScript هستند.

رویدادِ HookPython SDKTypeScript SDKچه چیزی آن را شلیک می‌کندنمونه‌ی کاربرد
PreToolUseبلهبلهدرخواستِ فراخوانیِ ابزار (می‌تواند مسدود یا تغییر دهد)مسدودکردنِ دستورهای خطرناکِ shell
PostToolUseبلهبلهنتیجه‌ی اجرای ابزارلاگ‌کردنِ همه‌ی تغییراتِ فایل در audit trail
PostToolUseFailureبلهبلهشکستِ اجرای ابزارمدیریت یا لاگ‌کردنِ خطاهای ابزار
PostToolBatchخیربلهیک batchِ کاملِ فراخوان‌های ابزار حل می‌شود، یک‌بار به‌ازای هر batch پیش از فراخوانیِ بعدیِ مدلتزریقِ یک‌باره‌ی قراردادها برای کلِ batch
UserPromptSubmitبلهبلهارسالِ پرامپتِ userتزریقِ کانتکستِ اضافی به پرامپت‌ها
MessageDisplayخیربلهیک پیامِ assistant با متن کامل می‌شود، یک‌بار به‌ازای هر پیام با متنِ کاملِ پیامسانسور یا قالب‌بندیِ مجددِ متنِ نمایش‌داده‌شده بدونِ تغییرِ transcript
Stopبلهبلهتوقفِ اجرای ایجنتذخیره‌ی stateِ نشست پیش از خروج
SubagentStartبلهبلهراه‌اندازیِ ساب‌ایجنتردگیریِ spawnِ موازیِ وظایف
SubagentStopبلهبلهاتمامِ ساب‌ایجنتتجمیعِ نتیجه‌ها از وظایفِ موازی
PreCompactبلهبلهدرخواستِ فشرده‌سازیِ گفتگوآرشیوِ transcriptِ کامل پیش از خلاصه‌سازی
PermissionRequestبلهبلهدیالوگِ مجوز نمایش داده می‌شدمدیریتِ سفارشیِ مجوز
SessionStartخیربلهراه‌اندازیِ نشستراه‌اندازیِ لاگ و telemetry
SessionEndخیربلهخاتمه‌ی نشستپاک‌سازیِ منابعِ موقت
Notificationبلهبلهپیام‌های وضعیتِ ایجنتفرستادنِ به‌روزرسانیِ وضعیتِ ایجنت به Slack یا PagerDuty
Setupخیربلهراه‌اندازی/نگهداریِ نشستاجرای وظایفِ راه‌اندازی
TeammateIdleخیربلهیک teammate بیکار می‌شودبازتخصیصِ کار یا اطلاع‌رسانی
TaskCompletedخیربلهیک وظیفه‌ی پس‌زمینه کامل می‌شودتجمیعِ نتیجه‌ها از وظایفِ موازی
ConfigChangeخیربلهفایلِ پیکربندی تغییر می‌کندبارگذاریِ مجددِ پویای تنظیمات
WorktreeCreateخیربلهgit worktree ساخته شدردگیریِ فضاهای کاریِ ایزوله
WorktreeRemoveخیربلهgit worktree حذف شدپاک‌سازیِ منابعِ فضای کاری

برای پیکربندیِ یک hook، آن را در فیلدِ hooksِ options‌های ایجنتت پاس بده (ClaudeAgentOptions در Python، شیءِ options در TypeScript):

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) است که در آن:

از matcherها برای فیلترِ زمانِ شلیک شدنِ callbackهایت استفاده کن. فیلدِ matcher بسته به نوعِ رویدادِ hook در برابرِ مقدارِ متفاوتی منطبق می‌شود. مثلاً hookهای مبتنی بر ابزار در برابرِ نامِ ابزار منطبق می‌شوند، در حالی که hookهای Notification در برابرِ نوعِ اعلان منطبق می‌شوند. برای فهرستِ کاملِ مقادیرِ matcher برای هر نوعِ رویداد به مرجعِ hookهای Claude Code نگاه کن.

matcherهای SDK از همان قواعدِ matcherها در فایل‌های تنظیمات پیروی می‌کنند: یک matcher که فقط شاملِ حروف، ارقام، _ و | باشد به‌عنوانِ رشته‌ی دقیق مقایسه می‌شود، با | که جایگزین‌ها را جدا می‌کند، پس Write|Edit دقیقاً همان دو ابزار را منطبق می‌کند. یک matcherِ *، یک رشته‌ی خالی، یا حذفِ کاملِ matcher هر رخدادِ آن رویداد را منطبق می‌کند؛ یک matcher که شاملِ هر کاراکترِ دیگری باشد به‌عنوانِ یک regular expression ارزیابی می‌شود، پس ^mcp__ هر ابزارِ MCP را منطبق می‌کند. یک matcher مثلِ mcp__memory فقط شاملِ حروف و زیرخط است، پس به‌عنوانِ رشته‌ی دقیق مقایسه می‌شود و هیچ ابزاری را منطبق نمی‌کند؛ از mcp__memory__.* استفاده کن تا هر ابزار از آن سرور را منطبق کنی.

گزینهنوعپیش‌فرضتوضیح
matcherstringundefinedالگویی که در برابرِ فیلدِ فیلترِ رویداد منطبق می‌شود، طبقِ قواعدِ مقایسه‌ی بالا. برای hookهای ابزار، این نامِ ابزار است. ابزارهای توکار شاملِ Bash, Read, Write, Edit, Glob, Grep, WebFetch, Agent و دیگران هستند (برای فهرستِ کامل به Tool Input Types نگاه کن). ابزارهای MCP از الگوی mcp__<server>__<action> استفاده می‌کنند.
hooksHookCallback[]-الزامی. آرایه‌ای از توابعِ callback که وقتی الگو منطبق شد اجرا شوند
timeoutnumber60timeout بر حسبِ ثانیه

هرجا ممکن است از الگوی matcher استفاده کن تا ابزارهای مشخص را هدف بگیری. یک matcher با 'Bash' فقط برای دستورهای Bash اجرا می‌شود، در حالی که حذفِ الگو callbackهایت را برای هر رخدادِ رویداد اجرا می‌کند. توجه کن که برای hookهای مبتنی بر ابزار، matcherها فقط بر اساسِ نامِ ابزار فیلتر می‌کنند، نه بر اساسِ مسیرهای فایل یا دیگر آرگومان‌ها. برای فیلتر بر اساسِ مسیرِ فایل، tool_input.file_path را درونِ callbackت بررسی کن.

هر hook callback سه آرگومان دریافت می‌کند:

  • داده‌ی ورودی: یک شیءِ تایپ‌شده که جزئیاتِ رویداد را دربردارد. هر نوعِ 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 هستند.
  • Tool use ID (str | None / string | undefined): رویدادهای PreToolUse و PostToolUse را برای همان فراخوانیِ ابزار همبسته می‌کند.
  • Context: در TypeScript، یک ویژگیِ signal (AbortSignal) برای لغو دربردارد. در Python، این آرگومان برای استفاده‌ی آینده رزرو شده.

callbackِ تو شیئی با دو دسته فیلد برمی‌گرداند:

  • فیلدهای سطحِ بالا روی هر رویداد یکسان کار می‌کنند: systemMessage پیامی به کاربر نشان می‌دهد، و continue (continue_ در Python) تعیین می‌کند که آیا ایجنت پس از این hook به اجرا ادامه دهد.
  • hookSpecificOutput عملیاتِ جاری را کنترل می‌کند. فیلدهای درونش به نوعِ رویدادِ hook بستگی دارند. برای hookهای PreToolUse، اینجا جایی است که permissionDecision ("allow", "deny", "ask" یا "defer"permissionDecisionReason و updatedInput را تنظیم می‌کنی. برگرداندنِ "defer" query را پایان می‌دهد تا بتوانی بعداً resume‌اش کنی. برای hookهای PostToolUse، می‌توانی additionalContext را تنظیم کنی تا اطلاعاتی به نتیجه‌ی ابزار بیفزایی. برای جایگزینیِ خروجیِ ابزار پیش از آنکه Claude آن را ببیند، updatedToolOutput را تنظیم کن، که برای هر ابزار در هر دو SDK کار می‌کند. فیلدِ قدیمی‌ترِ updatedMCPToolOutput فقط خروجیِ ابزارهای MCP را جایگزین می‌کند و منسوخ شده.

برای اجازه‌دادنِ عملیات بدونِ تغییر {} برگردان. callback hookهای SDK از همان فرمتِ خروجیِ JSONِ shell command hookهای Claude Code استفاده می‌کنند، که هر فیلد و گزینه‌ی خاصِ هر رویداد را مستند می‌کند. برای تعریف‌های نوعِ SDK، به مرجعِ TypeScript و Python SDK نگاه کن.

به‌طور پیش‌فرض، ایجنت پیش از ادامه منتظرِ بازگشتِ hookت می‌ماند. اگر hookت یک اثرِ جانبی انجام می‌دهد (لاگ، فرستادنِ webhook) و نیازی به تأثیرگذاری بر رفتارِ ایجنت ندارد، می‌توانی به‌جایش یک خروجیِ async برگردانی. این به ایجنت می‌گوید بلافاصله ادامه دهد بدونِ منتظرماندن برای اتمامِ hook:

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 };
};
فیلدنوعتوضیح
asynctrueحالتِ async را علامت می‌زند. ایجنت بدونِ منتظرماندن ادامه می‌دهد. در Python، از async_ استفاده کن تا از کلمه‌ی کلیدیِ رزروشده پرهیز کنی.
asyncTimeoutnumbertimeoutِ اختیاری بر حسبِ میلی‌ثانیه برای عملیاتِ پس‌زمینه

ورودیِ ابزار را تغییر بده

Section titled “ورودیِ ابزار را تغییر بده”

این مثال فراخوان‌های ابزارِ Write را رهگیری می‌کند و آرگومانِ file_path را بازنویسی می‌کند تا /sandbox را پیش‌اش بیفزاید، و همه‌ی نوشتن‌های فایل را به یک دایرکتوریِ sandboxشده تغییرِ مسیر می‌دهد. callback 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 {};
};

کانتکست بیفزا و یک ابزار را مسدود کن

Section titled “کانتکست بیفزا و یک ابزار را مسدود کن”

این مثال نوشتن‌ها در دایرکتوریِ /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 {
    # 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 {};
    };

ابزارهای مشخص را خودکار تأیید کن

Section titled “ابزارهای مشخص را خودکار تأیید کن”

به‌طور پیش‌فرض، ایجنت ممکن است پیش از استفاده از برخی ابزارها برای مجوز درخواست کند. این مثال ابزارهای فقط‌خواندنیِ filesystem (Read, Glob, Grep) را با برگرداندنِ permissionDecision: 'allow' خودکار تأیید می‌کند، تا بدونِ تأییدِ کاربر اجرا شوند در حالی که همه‌ی ابزارهای دیگر تابعِ بررسی‌های عادیِ مجوز می‌مانند:

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

وقتی یک رویداد شلیک می‌شود، همه‌ی hookهای منطبق به‌صورتِ موازی اجرا می‌شوند. برای تصمیم‌های مجوز، محدودکننده‌ترین نتیجه برنده است: یک denyِ تنها فراخوانیِ ابزار را فارغ از آنچه دیگر hookها برمی‌گردانند مسدود می‌کند. چون ترتیبِ اتمام قطعی نیست، هر hook را طوری بنویس که مستقل عمل کند، نه آنکه به اجرای زودترِ hookِ دیگری متکی باشد.

مثالِ زیر سه بررسیِ مستقل را برای هر فراخوانیِ ابزار ثبت می‌کند:

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های چندابزاره فیلتر کن

Section titled “با matcherهای چندابزاره فیلتر کن”

از matcherهای چندابزاره استفاده کن تا یک callback را میانِ ابزارهای مرتبط به اشتراک بگذاری. این مثال سه matcher با scopeهای متفاوت ثبت می‌کند:

  • یک فهرستِ دقیقِ خط‌لوله‌جداشده (Write|Edit|Delete) file_security_hook را فقط برای ابزارهای تغییرِ فایل شلیک می‌کند.

  • یک regex (^mcp__) mcp_audit_hook را برای هر ابزارِ MCP که نامش با mcp__ شروع می‌شود شلیک می‌کند.

  • یک matcherِ حذف‌شده global_logger را برای هر فراخوانیِ ابزار فارغ از نام شلیک می‌کند.

    options = ClaudeAgentOptions(
    hooks={
    "PreToolUse": [
    # Match file modification tools
    HookMatcher(matcher="Write|Edit|Delete", 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|Delete", hooks: [fileSecurityHook] },
    // Match all MCP tools
    { matcher: "^mcp__", hooks: [mcpAuditHook] },
    // Match everything (no matcher)
    { hooks: [globalLogger] }
    ]
    }
    };

فعالیتِ ساب‌ایجنت را ردگیری کن

Section titled “فعالیتِ ساب‌ایجنت را ردگیری کن”

از hookهای SubagentStop استفاده کن تا نظارت کنی کِی ساب‌ایجنت‌ها کارشان را تمام می‌کنند. نوعِ کاملِ ورودی را در مرجعِ TypeScript و Python SDK ببین. این مثال هر بار که یک ساب‌ایجنت کامل می‌شود یک خلاصه لاگ می‌کند:

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ها درخواستِ HTTP بفرست

Section titled “از hookها درخواستِ HTTP بفرست”

hookها می‌توانند عملیاتِ ناهمگام مثلِ درخواست‌های HTTP انجام دهند. خطاها را درونِ hookت بگیر به‌جای آنکه بگذاری منتشر شوند، چون یک exceptionِ مدیریت‌نشده می‌تواند ایجنت را قطع کند.

این مثال پس از کامل‌شدنِ هر ابزار یک webhook می‌فرستد و لاگ می‌کند کدام ابزار و کِی اجرا شد. hook خطاها را می‌گیرد تا یک webhookِ ناموفق ایجنت را قطع نکند:

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

اعلان‌ها را به Slack فوروارد کن

Section titled “اعلان‌ها را به Slack فوروارد کن”

از hookهای Notification استفاده کن تا اعلان‌های سیستمی را از ایجنت دریافت کنی و آن‌ها را به سرویس‌های بیرونی فوروارد کنی. اعلان‌ها برای نوع‌های رویدادی مثلِ این‌ها شلیک می‌شوند:

  • permission_prompt وقتی Claude به مجوز نیاز دارد
  • idle_prompt وقتی Claude منتظرِ ورودی است
  • auth_success وقتی احراز هویت کامل می‌شود
  • elicitation_dialog, elicitation_complete و elicitation_response برای جریان‌های elicitationِ پرامپتِ کاربر

هر اعلان یک فیلدِ message با توصیفِ انسان‌خوان و به‌صورتِ اختیاری یک title دارد.

این مثال هر اعلان را به یک کانالِ Slack فوروارد می‌کند. به یک Slack incoming webhook URL نیاز دارد، که با افزودنِ یک app به Slack workspaceات و فعال‌کردنِ incoming webhooks می‌سازی:

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

مشکلاتِ رایج را برطرف کن

Section titled “مشکلاتِ رایج را برطرف کن”
  • مطمئن شو نامِ رویدادِ hook درست و حساس به حروفِ بزرگ/کوچک است (PreToolUse، نه preToolUse)
  • بررسی کن که الگوی matcherات دقیقاً نامِ ابزار را منطبق می‌کند
  • مطمئن شو hook زیرِ نوعِ رویدادِ درست در options.hooks است
  • برای hookهای غیرابزاری مثلِ Stop و SubagentStop، matcherها در برابرِ فیلدهای متفاوتی منطبق می‌شوند (به matcher patterns نگاه کن)
  • hookها ممکن است وقتی ایجنت به سقفِ max_turns می‌رسد شلیک نشوند چون نشست پیش از اینکه hookها بتوانند اجرا شوند پایان می‌یابد

matcher آن‌طور که انتظار می‌رود فیلتر نمی‌کند

Section titled “matcher آن‌طور که انتظار می‌رود فیلتر نمی‌کند”

matcherها فقط نام‌های ابزار را منطبق می‌کنند، نه مسیرهای فایل یا دیگر آرگومان‌ها. برای فیلتر بر اساسِ مسیرِ فایل، tool_input.file_path را درونِ hookت بررسی کن:

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 {};
};
  • مقدارِ timeout را در پیکربندیِ HookMatcher افزایش بده
  • از AbortSignalِ آرگومانِ سومِ callback استفاده کن تا در TypeScript لغو را با ظرافت مدیریت کنی

ابزار به‌طورِ غیرمنتظره مسدود شده

Section titled “ابزار به‌طورِ غیرمنتظره مسدود شده”
  • همه‌ی hookهای PreToolUse را برای بازگشتِ permissionDecision: 'deny' بررسی کن
  • به hookهایت لاگ اضافه کن تا ببینی چه permissionDecisionReasonی برمی‌گردانند
  • مطمئن شو الگوهای matcher بیش‌ازحد گسترده نیستند (یک matcherِ خالی همه‌ی ابزارها را منطبق می‌کند)

ورودیِ تغییریافته اعمال نشده

Section titled “ورودیِ تغییریافته اعمال نشده”
  • مطمئن شو updatedInput درونِ hookSpecificOutput است، نه در سطحِ بالا:

    return {
    hookSpecificOutput: {
    hookEventName: "PreToolUse",
    permissionDecision: "allow",
    updatedInput: { command: "new command" }
    }
    };
  • permissionDecision: 'allow' برگردان تا ورودیِ تغییریافته را خودکار تأیید کنی، یا 'ask' تا برای تأیید به کاربر نشانش دهی

  • hookEventName را در hookSpecificOutput بگنجان تا مشخص کند خروجی برای کدام نوعِ hook است

hookهای نشست در Python در دسترس نیستند

Section titled “hookهای نشست در Python در دسترس نیستند”

SessionStart و SessionEnd را می‌توان در TypeScript به‌عنوانِ SDK callback hook ثبت کرد، ولی در Python SDK در دسترس نیستند (HookEvent آن‌ها را حذف می‌کند). در Python، فقط به‌عنوانِ shell command hook که در فایل‌های تنظیمات تعریف شده‌اند در دسترس‌اند (مثلاً .claude/settings.json). برای بارگذاریِ shell command hookها از برنامه‌ی SDKات، منبعِ تنظیماتِ مناسب را با setting_sources یا settingSources بگنجان:

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

برای اجرای منطقِ راه‌اندازی به‌عنوانِ یک Python SDK callback به‌جایش، از اولین پیامِ client.receive_response() به‌عنوانِ trigger خودت استفاده کن.

چندبرابرشدنِ درخواست‌های مجوزِ ساب‌ایجنت

Section titled “چندبرابرشدنِ درخواست‌های مجوزِ ساب‌ایجنت”

وقتی چند ساب‌ایجنت spawn می‌کنی، هرکدام ممکن است به‌طور جداگانه مجوز بخواهد. ساب‌ایجنت‌ها به‌طور خودکار مجوزهای ایجنتِ والد را به ارث نمی‌برند. برای پرهیز از درخواست‌های مکرر، از hookهای PreToolUse استفاده کن تا ابزارهای مشخص را خودکار تأیید کنی، یا قواعدِ مجوزی پیکربندی کن که روی نشست‌های ساب‌ایجنت اعمال شوند.

حلقه‌های بازگشتیِ hook با ساب‌ایجنت‌ها

Section titled “حلقه‌های بازگشتیِ hook با ساب‌ایجنت‌ها”

یک hookِ UserPromptSubmit که ساب‌ایجنت spawn می‌کند می‌تواند حلقه‌های بی‌نهایت بسازد اگر آن ساب‌ایجنت‌ها همان hook را تریگر کنند. برای جلوگیری از این:

  • پیش از spawn، در ورودیِ hook به‌دنبالِ یک نشانگرِ ساب‌ایجنت بگرد
  • از یک متغیرِ مشترک یا stateِ نشست استفاده کن تا ردگیری کنی آیا پیش‌تر درونِ یک ساب‌ایجنت هستی
  • hookها را scope کن تا فقط برای نشستِ ایجنتِ سطحِ بالا اجرا شوند

systemMessage در خروجی ظاهر نمی‌شود

Section titled “systemMessage در خروجی ظاهر نمی‌شود”

فیلدِ systemMessage پیامی به کاربر نشان می‌دهد، نه به مدل. به‌طور پیش‌فرض SDK خروجیِ hook را در جریانِ پیام نمایان نمی‌کند، پس پیام ممکن است ظاهر نشود مگر آنکه includeHookEvents (include_hook_events در Python) را تنظیم کنی. برای پاس‌دادنِ کانتکست به مدل به‌جایش، additionalContext را برگردان.

اگر لازم است تصمیم‌های hook را به‌طور قابل‌اعتماد به برنامه‌ات نمایان کنی، آن‌ها را جداگانه لاگ کن یا از یک کانالِ خروجیِ اختصاصی استفاده کن.