رهگیری و کنترلِ رفتارِ ایجنت با hookها
Hookها توابعِ callback هستند که کدِ تو را در پاسخ به رویدادهای ایجنت اجرا میکنند، مثلِ فراخوانیشدنِ یک ابزار، شروعِ یک نشست، یا توقفِ اجرا. با hookها میتوانی:
- عملیاتِ خطرناک را مسدود کنی پیش از اجرایشان، مثلِ دستورهای مخربِ shell یا دسترسیِ غیرمجاز به فایل
- هر فراخوانیِ ابزار را لاگ و حسابرسی کنی برای انطباق، دیباگ، یا تحلیل
- ورودیها و خروجیها را دگرگون کنی تا دادهها را پاکسازی کنی، اعتبارنامه تزریق کنی، یا مسیرهای فایل را تغییرِ مسیر دهی
- برای اقداماتِ حساس تأییدِ انسانی بخواهی مثلِ نوشتن در دیتابیس یا فراخوانیِ API
- چرخهی عمرِ نشست را ردگیری کنی تا state را مدیریت کنی، منابع را پاک کنی، یا اعلان بفرستی
این راهنما پوشش میدهد که hookها چطور کار میکنند، چطور پیکربندیشان کنی، و مثالهایی برای الگوهای رایج مثلِ مسدودکردنِ ابزار، تغییرِ ورودیها، و فورواردکردنِ اعلانها فراهم میکند.
hookها چطور کار میکنند
Section titled “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 asynciofrom claude_agent_sdk import ( AssistantMessage, ClaudeSDKClient, ClaudeAgentOptions, HookMatcher, ResultMessage,)
# Define a hook callback that receives tool call detailsasync 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 typeconst 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); }}hookهای موجود
Section titled “hookهای موجود”SDK برای مراحلِ مختلفِ اجرای ایجنت hook فراهم میکند. برخی hookها در هر دو SDK موجودند، در حالی که برخی فقط در TypeScript هستند.
| رویدادِ Hook | Python SDK | TypeScript 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ها را پیکربندی کن
Section titled “hookها را پیکربندی کن”برای پیکربندیِ یک 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) است که در آن:
- کلیدها نامهای رویدادِ hook هستند (مثلاً
'PreToolUse','PostToolUse','Stop') - مقادیر آرایههایی از matcherها هستند که هرکدام یک الگوی فیلترِ اختیاری و توابعِ callbackِ تو را دربردارند
Matcherها
Section titled “Matcherها”از 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__.* استفاده کن تا هر ابزار از آن سرور را منطبق کنی.
| گزینه | نوع | پیشفرض | توضیح |
|---|---|---|---|
matcher | string | undefined | الگویی که در برابرِ فیلدِ فیلترِ رویداد منطبق میشود، طبقِ قواعدِ مقایسهی بالا. برای hookهای ابزار، این نامِ ابزار است. ابزارهای توکار شاملِ Bash, Read, Write, Edit, Glob, Grep, WebFetch, Agent و دیگران هستند (برای فهرستِ کامل به Tool Input Types نگاه کن). ابزارهای MCP از الگوی mcp__<server>__<action> استفاده میکنند. |
hooks | HookCallback[] | - | الزامی. آرایهای از توابعِ callback که وقتی الگو منطبق شد اجرا شوند |
timeout | number | 60 | timeout بر حسبِ ثانیه |
هرجا ممکن است از الگوی matcher استفاده کن تا ابزارهای مشخص را هدف بگیری. یک matcher با 'Bash' فقط برای دستورهای Bash اجرا میشود، در حالی که حذفِ الگو callbackهایت را برای هر رخدادِ رویداد اجرا میکند. توجه کن که برای hookهای مبتنی بر ابزار، matcherها فقط بر اساسِ نامِ ابزار فیلتر میکنند، نه بر اساسِ مسیرهای فایل یا دیگر آرگومانها. برای فیلتر بر اساسِ مسیرِ فایل، tool_input.file_path را درونِ callbackت بررسی کن.
توابعِ callback
Section titled “توابعِ callback”ورودیها
Section titled “ورودیها”هر 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هستند.
- همهی ورودیهای hook فیلدهای
- Tool use ID (
str | None/string | undefined): رویدادهایPreToolUseوPostToolUseرا برای همان فراخوانیِ ابزار همبسته میکند. - Context: در TypeScript، یک ویژگیِ
signal(AbortSignal) برای لغو دربردارد. در Python، این آرگومان برای استفادهی آینده رزرو شده.
خروجیها
Section titled “خروجیها”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 نگاه کن.
خروجیِ ناهمگام
Section titled “خروجیِ ناهمگام”بهطور پیشفرض، ایجنت پیش از ادامه منتظرِ بازگشتِ 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 };};| فیلد | نوع | توضیح |
|---|---|---|
async | true | حالتِ async را علامت میزند. ایجنت بدونِ منتظرماندن ادامه میدهد. در Python، از async_ استفاده کن تا از کلمهی کلیدیِ رزروشده پرهیز کنی. |
asyncTimeout | number | timeoutِ اختیاری بر حسبِ میلیثانیه برای عملیاتِ پسزمینه |
مثالها
Section titled “مثالها”ورودیِ ابزار را تغییر بده
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 usersystemMessage: "Remember: system directories like /etc are protected.",// hookSpecificOutput: block the operationhookSpecificOutput: {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 را ثبت کن
Section titled “چند hook را ثبت کن”وقتی یک رویداد شلیک میشود، همهی 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 toolsHookMatcher(matcher="Write|Edit|Delete", hooks=[file_security_hook]),# Match all MCP toolsHookMatcher(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 asyncioimport jsonimport urllib.requestfrom 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 hookfor 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 asyncioimport jsonimport 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 Slackconst 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 شلیک نمیشود
Section titled “hook شلیک نمیشود”- مطمئن شو نامِ رویدادِ 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ِ hook
Section titled “timeoutِ hook”- مقدارِ
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 را بهطور قابلاعتماد به برنامهات نمایان کنی، آنها را جداگانه لاگ کن یا از یک کانالِ خروجیِ اختصاصی استفاده کن.
منابعِ مرتبط
Section titled “منابعِ مرتبط”- مرجعِ hookهای Claude Code: طرحهای کاملِ ورودی/خروجیِ JSON، مستنداتِ رویداد، و الگوهای matcher
- راهنمای hookهای Claude Code: مثالها و گامبهگامهای shell command hook
- مرجعِ TypeScript SDK: نوعهای hook، تعریفهای ورودی/خروجی، و گزینههای پیکربندی
- مرجعِ Python SDK: نوعهای hook، تعریفهای ورودی/خروجی، و گزینههای پیکربندی
- دسترسیها: کنترل کن ایجنتت چه کاری بتواند بکند
- ابزارهای سفارشی: ابزار بساز تا قابلیتهای ایجنت را گسترش دهی