מדריך קלוד קוד בעברית

תיעוד 186

יירוט ושליטה בהתנהגות הסוכן באמצעות hooks

יירוט והתאמה אישית של התנהגות הסוכן בנקודות ביצוע מרכזיות באמצעות hooks

Hooks הן פונקציות callback שמריצות את הקוד שלך בתגובה לאירועי הסוכן, כמו קריאה לכלי, התחלת סשן, או עצירת ביצוע. באמצעות hooks אפשר:

  • לחסום פעולות מסוכנות לפני שהן מתבצעות, כמו פקודות shell הרסניות או גישה לא מורשית לקבצים
  • לתעד ולבקר קריאות לכלים לצורך תאימות, ניפוי שגיאות או אנליטיקה
  • לשנות קלטים ופלטים כדי לנקות נתונים, להזריק אישורים, או להפנות נתיבי קבצים
  • לדרוש אישור אנושי לפעולות רגישות כמו כתיבה למסד נתונים או קריאות API
  • לעקוב אחר מחזור החיים של הסשן כדי לנהל מצב, לנקות משאבים, או לשלוח התראות

#איך hooks עובדים

  1. אירוע מופעל

משהו קורה במהלך ביצוע הסוכן וה-SDK מפעיל אירוע: כלי עומד להיקרא (PreToolUse), כלי החזיר תוצאה (PostToolUse), תת סוכן התחיל או נעצר, הסוכן במצב idle, או שהביצוע הסתיים. ראה את הרשימה המלאה של האירועים.

  1. ה-SDK אוסף hooks רשומים

ה-SDK בודק hooks שנרשמו לסוג האירוע הזה. זה כולל callback hooks שאתה מעביר ב-options.hooks ו-hooks של פקודות shell מקובצי הגדרות כאשר הרשומה המתאימה settingSources או setting_sources מופעלת, והיא מופעלת עבור אפשרויות ברירת המחדל של query().

  1. Matchers מסננים אילו hooks רצים

אם ל-hook יש תבנית matcher (כמו "Write|Edit"), ה-SDK בודק אותה מול היעד של האירוע (לדוגמה, שם הכלי). Hooks בלי matcher רצים עבור כל אירוע מהסוג הזה.

  1. פונקציות callback מתבצעות

פונקציית ה-callback של כל hook תואם מקבלת קלט על מה שקורה: שם הכלי, הארגומנטים שלו, מזהה הסשן, ופרטים נוספים שתלויים באירוע.

  1. ה-callback שלך מחזיר החלטה

אחרי ביצוע פעולות כלשהן (תיעוד, קריאות API, אימות), ה-callback מחזיר אובייקט פלט שאומר לסוכן מה לעשות: לאפשר את הפעולה, לחסום אותה, לשנות את הקלט, או להזריק הקשר לשיחה.

הדוגמה הזו רושמת hook מסוג PreToolUse (שלב 1) עם matcher של "Write|Edit" (שלב 3) כך שה-callback מופעל עבור הכלים Write ו-Edit. כשהוא מופעל, ה-callback מקבל את הקלט של הכלי (שלב 4), בודק אם נתיב הקובץ מכוון לקובץ .env, ומחזיר permissionDecision: "deny" כדי לחסום את הפעולה (שלב 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.lower() == ".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?.toLowerCase() === ".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);
  }
}

ה-hook דוחה קריאת Write או Edit שה-file_path שלה מסתיים ב-/.env, בכל מצב אותיות. ב-Windows, file_path משתמש בלוכסנים הפוכים, כמו C:\project\.env, וה-hook הזה מאפשר קריאת Write או Edit אל C:\project\.env.

[!WARNING]

המתאם Write|Edit מגביל את ה-hook הזה לכלי Write ו-Edit. אם Claude כותב .env באמצעות פקודת Bash, ה-hook לא רץ. כדי לחסום כתיבות Bash לקובץ במקום שבו ארגז החול רץ, ראה את הגדרת ארגז החול עבור Python או TypeScript.

#Hooks זמינים

ה-SDK מספק hooks לשלבים שונים של ביצוע הסוכן. חלק מה-hooks זמינים בשני ה-SDK, ואחרים זמינים רק ב-TypeScript.

אירוע HookPython SDKTypeScript SDKמה מפעיל אותודוגמה לשימוש
PreToolUseכןכןבקשת קריאה לכלי (אפשר לחסום או לשנות)חסימת פקודות shell מסוכנות
PostToolUseכןכןתוצאת ביצוע של כליתיעוד שינויי קבצים ביומן ביקורת
PostToolUseFailureכןכןכשל בביצוע כליטיפול בשגיאות כלי או תיעוד שלהן
PostToolBatchלאכןאצווה מלאה של קריאות לכלים מסתיימת, פעם אחת לכל אצווה לפני קריאת המודל הבאההזרקת מוסכמות פעם אחת עבור כל האצווה
UserPromptSubmitכןכןפרומפט נשלח, כולל תור ש-Claude Code מתחיל בעצמוהזרקת הקשר נוסף לפרומפטים
UserPromptExpansionלאכןפקודה שהמשתמש הקליד, או פרומפט MCP, מתרחבת לפרומפט לפני שהיא מגיעה ל-Claude. לא מופעל כש-Claude מפעיל skill בעצמוחסימת פקודה מהפעלה ישירה או הוספת הקשר כש-skill מוקלד
MessageDisplayלאכןהודעת assistant עם טקסט מסתיימת, פעם אחת לכל הודעה עם הטקסט המלא של ההודעההשחרה או עיצוב מחדש של הטקסט המוצג בלי לשנות את התמליל
Stopכןכןעצירת ביצוע הסוכןשמירת מצב הסשן לפני יציאה
StopFailureלאכןהתור מסתיים בשגיאת API במקום עצירה רגילהתיעוד כשלים או שליחת התראות
SubagentStartכןכןאתחול תת סוכןמעקב אחרי יצירת משימות במקביל
SubagentStopכןכןהשלמת תת סוכןצבירת תוצאות ממשימות מקבילות
PreCompactכןכןבקשת דחיסת שיחהארכוב התמליל המלא לפני סיכום
PostCompactלאכןדחיסת השיחה מסתיימתתיעוד הסיכום שנוצר
PreModelSwitchלאכןהחלפת מודל שהתבקשה, לפני שהיא מתרחשת (אפשר לחסום)חסימת מעבר למודל מסוים
PostModelSwitchלאכןהמודל של הסשן משתנה, כולל fallback אוטומטימתן הנחיה ל-Claude שמותאמת למודל החדש
PermissionRequestכןכןקריאה לכלי זקוקה להחלטת הרשאהטיפול מותאם בהרשאות
PermissionDeniedלאכןמצב Auto דוחה קריאה לכלי, כולל דחיות בלי פסק דין של מסווגתיעוד דחיות, או אמירה למודל שהוא רשאי לנסות שוב. Claude Code מתעלם מ-retry: true בדחיות בלי פסק דין. ראה PermissionDenied
SessionStartלאכןאתחול סשןאתחול תיעוד וטלמטריה
SessionEndלאכןסיום סשןניקוי משאבים זמניים
Notificationכןכןהודעות סטטוס של הסוכןשליחת עדכוני סטטוס של הסוכן ל-Slack או ל-PagerDuty
Setupלאכןהגדרה או תחזוקה של סשןהרצת משימות אתחול
TeammateIdleלאכןחבר צוות עובר למצב idleהקצאה מחדש של עבודה או שליחת התראה
TaskCreatedלאכןמשימה נוצרת באמצעות הכלי TaskCreateאכיפת מוסכמות שמות למשימות
TaskCompletedלאכןמשימה מסומנת כהושלמהדרישה שבדיקות יעברו לפני שמשימה נסגרת
Elicitationלאכןשרת MCP מבקש קלט משתמש באמצע משימהמענה פרוגרמטי לבקשות קלט של MCP
ElicitationResultלאכןמשתמש מגיב ל-elicitation של MCPשינוי או חסימה של התשובה לפני שהיא חוזרת לשרת
ConfigChangeלאכןשינויים בקובץ תצורהטעינה מחדש של הגדרות באופן דינמי
InstructionsLoadedלאכןקובץ CLAUDE.md או קובץ כללים נטען להקשרביקורת על אילו קובצי הוראות נטענים
WorktreeCreateלאכןנוצר worktree של Gitמעקב אחרי סביבות עבודה מבודדות
WorktreeRemoveלאכןworktree שנוצר על ידי hook מסוג WorktreeCreate מוסרניקוי משאבי סביבת העבודה
CwdChangedלאכןספריית העבודה משתנה במהלך סשןטעינה מחדש של משתני סביבה לפי ספרייה
FileChangedלאכןקובץ במעקב משתנה, נוצר, או נמחקטעינה מחדש של תצורה כשקובצי הפרויקט משתנים
DirectoryAddedלאכןספריית עבודה מתווספת במהלך סשןהתקנת תלויות עבור מאגר שנוסף באמצע הסשן

#הגדרת hooks

כדי להגדיר hook, מעבירים אותו בשדה hooks של אפשרויות הסוכן (ClaudeAgentOptions ב Python, אובייקט options ב TypeScript). קטע הקוד הזה מניח שכבר הגדרתם callback של hook, כמו protect_env_files ב Python או protectEnvFiles ב 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, ובו:

#Matchers

משתמשים ב matchers כדי לסנן מתי ה callbacks שלכם מופעלים. השדה matcher מותאם לערך שונה לפי סוג אירוע ה hook. לדוגמה, hooks מבוססי כלי מותאמים לשם הכלי, ו hooks מסוג Notification מותאמים לסוג ההתראה.

matchers של ה SDK פועלים לפי אותם כללים כמו matchers בקבצי הגדרות. הסעיף הזה מתעד את מסלולי ההערכה של מחרוזת מדויקת ושל ביטוי רגולרי, את דרישות הגרסה שלהם, ואת ערכי ה matcher לכל סוג אירוע.

אפשרותסוגברירת מחדלתיאור
matcherstringundefinedתבנית שמותאמת לשדה הסינון של האירוע, לפי הכללים ל matchers בקבצי הגדרות. עבור hooks של כלים, השדה הזה הוא שם הכלי. כלים מובנים כוללים את Bash, Read, Write, Edit, Glob, Grep, WebFetch, Agent, ואחרים. ראו Tool Input Types לרשימה המלאה. כלי MCP משתמשים בתבנית mcp__<server>__<action>, שבה <server> הוא המפתח שבו משתמשים בהגדרת mcpServers, באיות שמתאר tool naming convention.
hooksHookCallback[]-חובה. מערך של פונקציות callback להרצה כשהתבנית מתאימה
timeoutnumberundefinedזמן קצוב בשניות. כשמשמיטים אותו, Claude Code מחיל את זמן הקצוב ברירת המחדל של האירוע. ה callbacks של ה SDK שלכם פועלים לפי ברירות המחדל של hook מסוג command
onFailure"continue" | "block"undefinedמה עושה callback שנכשל. הגדירו "block" כדי לחסום את הפעולה כש callback נכשל. ב Python משתמשים ב on_failure

השתמשו בתבנית matcher כדי לכוון לכלים מסוימים בכל פעם שאפשר. matcher עם 'Bash' רץ רק לפקודות Bash, והשמטת התבנית מריצה את ה callbacks שלכם בכל מופע של האירוע.

#פונקציות callback

#קלטים

כל callback של hook מקבל שלושה ארגומנטים:

  • נתוני קלט: אובייקט מטיפוס שמכיל פרטי אירוע. לכל סוג hook יש צורת קלט משלו. לדוגמה, PreToolUseHookInput כולל את tool_name ואת tool_input, ו NotificationHookInput כולל את message. ראו את הגדרות הטיפוסים המלאות בהפניות ה SDK של TypeScript ושל Python.
    • כל קלטי ה hook חולקים את session_id, cwd, וגם hook_event_name.
    • agent_id ו agent_type מתמלאים כשה hook מופעל בתוך subagent. ב TypeScript, אלה נמצאים על קלט ה hook הבסיסי וזמינים לכל סוגי ה hook. ב Python, אלה שדות אופציונליים ב PreToolUse, PostToolUse, PostToolUseFailure, ו PermissionRequest, ושדות חובה ב SubagentStart ו SubagentStop.
  • מזהה שימוש בכלי (str | None / string | undefined): מקשר אירועי PreToolUse ו PostToolUse לאותה קריאת כלי.
  • הקשר: ב TypeScript, מכיל מאפיין signal (AbortSignal) לביטול. ב Python, הארגומנט הזה שמור לשימוש עתידי.

#פלטים

ה callback שלכם מחזיר אובייקט עם שתי קטגוריות של שדות:

  • שדות ברמה העליונה מתקבלים בכל אירוע: systemMessage מציג הודעה למשתמש, ו continue (continue_ ב Python) קובע אם הסוכן ממשיך לרוץ אחרי ה hook הזה. חלק מהאירועים משליכים אותם או מעבירים אותם למקום אחר. הסעיף של כל אירוע בדף ה hooks אומר לאן הם מגיעים.
  • hookSpecificOutput שולט בפעולה הנוכחית. השדות שמגדירים בתוכו תלויים בסוג אירוע ה hook:
    • עבור hooks מסוג PreToolUse, כאן מגדירים את permissionDecision ("allow", "deny", "ask", או "defer"), את permissionDecisionReason, ואת updatedInput. אם מחזירים "defer", התור מסתיים בהודעת תוצאה שבה stop_reason הוא "tool_deferred", כך שאפשר לחדש את הקריאה מאוחר יותר.
    • עבור hooks מסוג PostToolUse, אפשר להגדיר additionalContext כדי לצרף מידע לתוצאת הכלי. כדי להחליף את הפלט של הכלי לפני ש Claude רואה אותו, מגדירים updatedToolOutput, שעובד לכל כלי בשני ה SDK. השדה הישן יותר updatedMCPToolOutput מחליף פלט של כלי MCP בלבד.
    • ב SDK של TypeScript, callback של PostToolUse יכול גם להחזיר classifierContext, הערה קצרה על תוצאת קריאת הכלי עבור מסווג ההרשאות של auto mode. מכיוון שה callback שלכם רץ בתהליך של היישום עצמו, המסווג עשוי לשקול אמירת משתמש שאתם מעבירים בהערה ככוונת משתמש. השדה דורש TypeScript Agent SDK מגרסה v0.3.236 ואילך. Annotate a result for the auto mode classifier מכסה את תקרת האורך, את הכלל של סינכרוני בלבד, ומה לא לשים בהערה.

מחזירים {} כדי לאפשר את הפעולה בלי שינויים. hooks של callback ב SDK משתמשים באותו פורמט פלט JSON כמו hooks של פקודת מעטפת ב Claude Code, שמתעד כל שדה ואפשרות ספציפית לאירוע. להגדרות הטיפוסים של ה SDK, ראו את הפניות ה SDK של TypeScript ושל Python.

[!NOTE]

כשמספר hooks מסוג PreToolUse מחזירים החלטות שונות, ל deny יש עדיפות על defer, ל defer יש עדיפות על ask, ול ask יש עדיפות על allow.

#פלט אסינכרוני

כברירת מחדל, הסוכן ממתין שה hook שלכם יחזור לפני שהוא ממשיך. אם ה hook מבצע תופעת לוואי, כמו תיעוד ביומן או שליחת webhook, ולא צריך להשפיע על התנהגות הסוכן, אפשר להחזיר פלט אסינכרוני. זה אומר לסוכן להמשיך מיד בלי להמתין שה hook יסתיים. בקטע הזה, send_to_logging_service ב Python ו sendToLoggingService ב TypeScript מייצגים כל פונקציית תיעוד שאתם מגדירים:

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מסמן מצב אסינכרוני. הסוכן ממשיך בלי להמתין. ב Python משתמשים ב async_ כדי להימנע ממילת המפתח השמורה.
asyncTimeoutnumberזמן קצוב אופציונלי, במילישניות, לפעולת הרקע

[!NOTE]

פלטים אסינכרוניים לא יכולים לחסום, לשנות, או להזריק הקשר לפעולה, כי הסוכן כבר המשיך הלאה. השתמשו בהם רק לתופעות לוואי כמו תיעוד ביומן, מדדים, או התראות.

#חסימת הפעולה כש callback נכשל

ברוב האירועים, אם callback זורק שגיאה או מחזיר פלט שנכשל באימות, הפעולה ממשיכה והיישום שלכם מקבל אזהרת SDKInformationalMessage שבה tag הוא hook_callback_failed. guard של PreToolUse שקורס מאפשר אז לקריאת הכלי להגיע לזרימת ההרשאות הרגילה. callback שלא מחזיר כלום, כמו נתיב בלי return, נחשב גם הוא לכישלון. ב Python, קוראים את tag מ SystemMessage.data.

כדי לחסום את הפעולה, מגדירים onFailure: "block" ב TypeScript או on_failure="block" ב Python על ה matcher. נדרש TypeScript Agent SDK מגרסה v0.3.317 ואילך, או Python Agent SDK מגרסה v0.2.177 ואילך. הדוגמה הזו חוסמת קריאת Bash כש check_policy ב Python או checkPolicy ב TypeScript נכשל:

options = ClaudeAgentOptions(
    hooks={
        "PreToolUse": [
            HookMatcher(matcher="Bash", hooks=[check_policy], on_failure="block")
        ]
    }
)
import type { Options } from "@anthropic-ai/claude-agent-sdk";

const options: Options = {
  hooks: {
    PreToolUse: [{ matcher: "Bash", hooks: [checkPolicy], onFailure: "block" }]
  }
};

כדי לבדוק את זה, גורמים ל callback להעלות שגיאה ומבקשים מ Claude להריץ פקודת Bash. תוצאת הכלי כוללת את failed; blocking because onFailure is "block", ו Claude לא רואה את טקסט השגיאה שלכם.

מחזירים {} כשל callback אין עמדה. כש "block" מוגדר, נתיב בלי return חוסם את הפעולה. callback שתופס את השגיאה שלו ומחזיר {} לא נכשל, לכן תנו לשגיאה להתפשט מתוך guard.

#אירועים שמתנהגים אחרת

האירועים האלה לא תמיד חוסמים כש "block" מוגדר:

  • Stop, SubagentStop, TaskCompleted, ו TeammateIdle: האירועים האלה מתעלמים מהאפשרות, ולכן הפעולה ממשיכה.
  • PermissionRequest: החסימה נחשבת רק אם היא מגיעה לפני שה callback של canUseTool עונה.
  • PostToolUse: הכלי כבר רץ, ולכן Claude מקבל את הכישלון כמשוב.

callback שחורג מזמן הקצוב שלו פועל לפי Hook timeout. כש "block" מוגדר, זמן קצוב חוסם גם את PreCompact, Elicitation, ו WorktreeCreate.

#שגיאות הפעלה עבור onFailure

כש "block" מוגדר, השאילתה נכשלת בהפעלה ולא שולחת prompt כשהיא לא יכולה לכבד את האפשרות:

  • Claude Code לא תומך באפשרות: השגיאה כוללת את does not honor. עדכנו את Claude Code, או הסירו את האפשרות.
  • הסשן כבר רץ ולא רשם את ה hooks שלכם: השגיאה כוללת את did not register your hooks. התחילו סשן חדש.

#דוגמאות

כמה דוגמאות בסעיף הזה מציגות רק את פונקציית ה-callback. כדי להריץ אחת, רשום את ה-callback תחת האירוע המתאים בשדה hooks של האפשרויות שלך, כמו שמוצג בהגדרת hooks.

#שינוי קלט של כלי

הדוגמה הזו מיירטת קריאות לכלי Write וכותבת מחדש את הארגומנט file_path כך שיתווסף בתחילתו /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 {};
};

[!NOTE]

צמד את updatedInput עם permissionDecision: 'allow' כדי לאשר אוטומטית את הקלט ששונה, או עם permissionDecision: 'ask' כדי להציג אותו למשתמש. אם משמיטים את permissionDecision, הקלט ששונה עדיין חל ועובר דרך הערכת ההרשאות הרגילה. עם 'defer', מתעלמים מ-updatedInput. תמיד החזר אובייקט חדש במקום לשנות את tool_input המקורי.

כדי לאמת את ההפניה, הגדר את הקידומת לנתיב שאפשר לכתוב אליו, למשל ./sandbox או /tmp/sandbox (ב-macOS אי אפשר ליצור ספריית /sandbox ברמת השורש), ואז בקש מהסוכן לכתוב קובץ: תוצאת כלי ה-Write בזרם ההודעות מציינת את הנתיב עם קידומת ה-sandbox שלך ולא את זה ש-Claude ביקש.

#הוספת הקשר וחסימת כלי

הדוגמה הזו דוחה קריאות לכלי שה-file_path שלהן מתחיל ב-/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 {};
};

כדי לאמת את החסימה, רשום את ה-callback תחת PreToolUse עם matcher של Write|Edit ובקש מהסוכן ליצור קובץ תחת /etc: תוצאת כלי ה-Write בזרם ההודעות מכילה Writing to /etc is not allowed, ולא נוצר קובץ.

#אישור אוטומטי של כלים מסוימים

כברירת מחדל, הסוכן עשוי לבקש הרשאה לפני שימוש בכלים מסוימים. הדוגמה הזו מאשרת אוטומטית כלי מערכת קבצים לקריאה בלבד (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 {};
};

#רישום כמה hooks

כשאירוע מופעל, ה-hooks התואמים רצים במקביל. בהחלטות הרשאה, התוצאה המגבילה ביותר חלה. deny של hook אחד חוסם את קריאת הכלי. מכיוון שסדר ההשלמה אינו דטרמיניסטי, כתוב כל hook כך שיפעל באופן עצמאי ולא יסתמך על כך ש-hook אחר כבר רץ קודם.

הדוגמה למטה רושמת שלוש בדיקות עצמאיות, כל אחת עם matcher שהושמט. שמות ה-hooks שבה, למשל audit_logger ב-Python או auditLogger ב-TypeScript, מייצגים callbacks שאתה מגדיר:

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

#סינון עם matchers לכמה כלים

השתמש ב-matchers לכמה כלים כדי לשתף callback אחד בין כלים קשורים. הדוגמה הזו רושמת שלושה matchers עם היקפים שונים, וכל hook שהיא מציינת מייצג callback שאתה מגדיר:

  • רשימה מדויקת מופרדת בקו אנכי (Write|Edit|NotebookEdit) מפעילה את file_security_hook עבור הכלים Write, Edit ו-NotebookEdit.
  • ביטוי רגולרי (^mcp__) מפעיל את mcp_audit_hook עבור כל כלי MCP שהשם שלו מתחיל ב-mcp__.
  • matcher שהושמט מפעיל את global_logger בלי סינון לפי שם כלי.
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] }
    ]
  }
};

#מעקב אחרי פעילות של subagent

השתמש ב-hooks של SubagentStop כדי לעקוב מתי subagents מסיימים את העבודה שלהם. ראה את טיפוס הקלט המלא בעמודי העזר של ה-SDK ב-TypeScript וב-Python. הדוגמה הזו רושמת סיכום בכל פעם ש-subagent מסתיים:

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 מופעל, רשום את ה-callback ובקש מהסוכן להאציל משימה קטנה ל-subagent, למשל לרשום את הקבצים בתיקייה הנוכחית: כשה-subagent מסיים, ה-callback מדפיס את השורות [SUBAGENT] Completed: עם ה-ID של ה-subagent ונתיב התמליל.

#שליחת בקשות HTTP מתוך hooks

Hooks יכולים לבצע פעולות אסינכרוניות כמו בקשות HTTP. תפוס שגיאות בתוך ה-hook שלך במקום לתת להן להתפשט.

הדוגמה הזו שולחת 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
        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
  }

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

כדי לאמת שה-hook מופעל, כוון את כתובת ה-webhook לנקודת קצה שאפשר לצפות בה ושלח prompt שמשתמש בכלי: ה-hook שולח POST עם שם הכלי וחותמת זמן אחרי שכל כלי מסתיים.

#העברת התראות ל-Slack

השתמש ב-hooks של Notification כדי לקבל התראות מערכת מהסוכן ולהעביר אותן לשירותים חיצוניים. בסשנים של ה-SDK, Claude Code מריץ את ה-hook הזה עבור סוגי ההתראות הבאים:

  • permission_prompt אחרי שבקשת הרשאה המתינה כשש שניות ב-callback של canUseTool. דורש TypeScript Agent SDK בגרסה v0.3.233 או חדשה יותר, או Python Agent SDK בגרסה v0.2.139 או חדשה יותר
  • elicitation_complete ו-elicitation_response עבור תהליכי elicitation של בקשות משתמש
  • plugin_notification כש-mod קורא ל-$.ui.notify

Claude Code פולט את הסוגים האחרים, כמו idle_prompt, auth_success ו-elicitation_dialog, מממשק משתמש אינטראקטיבי שסשנים של ה-SDK לא מריצים.

כל התראה כוללת שדה message עם תיאור קריא לאדם, ולפעמים גם title.

הדוגמה הזו מעבירה כל התראה לערוץ Slack. היא דורשת כתובת incoming webhook של Slack, שיוצרים על ידי הוספת אפליקציה ל-workspace של Slack והפעלת 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);
}

כשאירוע Notification מופעל, ה-hook מפרסם את ה-message של ההתראה, עם הקידומת Agent status:, לערוץ שה-webhook שלך מכוון אליו.

#תיקון בעיות נפוצות

#Hook לא מופעל

  • ודאו ששם אירוע ה-hook נכון ורגיש לאותיות גדולות וקטנות (PreToolUse, לא preToolUse)
  • בדקו שתבנית ה-matcher תואמת את שם הכלי בדיוק
  • ודאו שה-hook נמצא תחת סוג האירוע הנכון ב-options.hooks
  • עבור hooks שאינם כלים ותומכים ב-matchers, כמו Notification ו-SubagentStop, matchers מותאמים מול שדות שונים, ו-Stop מתעלם מ-matchers לחלוטין (ראו תבניות matcher)
  • Hooks עשויים לא לפעול כשהסוכן מגיע למגבלת max_turns, כי הסשן מסתיים לפני ש-hooks יכולים לרוץ

#Matcher לא מסנן כמצופה

Matchers מתאימים רק לשמות כלים, לא לנתיבי קבצים או לארגומנטים אחרים. כדי לסנן לפי נתיב קובץ, בדקו את 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 {};
};

#פסק זמן של hook

Claude Code מריץ כל callback עם פסק זמן, שמגדירים בשניות בשדה timeout על ה-HookMatcher שלו. כשלא מגדירים פסק זמן, Claude Code משתמש בברירת המחדל של האירוע: 600 שניות לרוב האירועים, 30 שניות ל-UserPromptSubmit, ל-PreModelSwitch ול-PostModelSwitch, ו-10 שניות ל-MessageDisplay. Claude Code מריץ callbacks של SessionEnd במהלך הכיבוי תחת תקציב פסק הזמן של SessionEnd הקצר יותר, 1.5 שניות כברירת מחדל.

כש-callback חורג מפסק הזמן שלו, Claude Code מבטל אותו ומשליך את הפלט שלו, והסשן ממשיך בלי להיתקע. מה שקורה בהמשך תלוי באירוע:

  • PreToolUse: Claude Code חוסם את קריאת הכלי. אם כל hook אחר החזיר כלום או רק החלטת allow, תוצאת הכלי אומרת ל-Claude שה-hook לא הגיב לפני פסק הזמן, והתור ממשיך. אם hook אחר שלכם כבר דחה את הקריאה, הדחייה עדיין מגיעה ל-Claude: תוצאת הכלי כוללת את הסיבה של אותו hook, וטקסט פסק הזמן יכול לבוא אחריה.

    לפני v2.1.210, Claude Code דיווח על פסק הזמן ל-Claude כדחייה של המשתמש, וזה גרם לסשנים ללא השגחה לעצור ולהמתין לקלט.

  • PostToolUse ו-PostToolUseFailure: Claude Code שומר את תוצאת הכלי והתור ממשיך.

  • UserPromptSubmit ו-UserPromptExpansion: Claude Code חוסם את הפרומפט עם הודעה שמציינת את ה-hook ואת פסק הזמן, והסשן ממשיך. לפני v2.1.208, Claude Code סיים את השאילתה עם error_during_execution כש-callback באירועים האלה חרג מפסק הזמן.

  • Stop ו-SubagentStop: callback שחרג מפסק הזמן נספר כהחזרה בלי החלטה. הסוכן או תת-הסוכן עוצרים כאילו אותו callback התיר אותם, והחלטה מ-hooks אחרים שלכם על האירוע עדיין חלה. לפני Claude Code v2.1.273, callback של Stop או SubagentStop שחרג מפסק הזמן נספר כריצת hook שנכשלה, ו-Claude Code השליך את ההחלטות של hooks אחרים שלכם על האירוע.

  • SessionStart: callback שחרג מפסק הזמן נספר כהחזרה בלי פלט, והסשן ממשיך עם הפלט של hooks אחרים של SessionStart.

  • PreModelSwitch: Claude Code חוסם את החלפת המודל. Hook שלא ענה לא אישר את ההחלפה.

  • אירועים אחרים, כמו Notification, PreCompact ו-PostModelSwitch: Claude Code רושם את הכשל ביומן וממשיך. עם onFailure: "block", פסק זמן ב-PreCompact, ב-Elicitation או ב-WorktreeCreate חוסם את הפעולה.

בפעם הראשונה ש-callback של Stop או SessionStart חורג מפסק הזמן בסשן הראשי, Claude Code גם מוסיף SDKInformationalMessage לזרם ההודעות, ובו נאמר שהאפליקציה שמניעה את הסשן לא הגיבה. פסקי זמן מאוחרים יותר לא חוזרים על ההודעה הזאת כל עוד האפליקציה נשארת בלי תגובה.

אם קוטעים את השאילתה בזמן ש-callback ממתין, Claude Code מבטל את קריאת הכלי הממתינה. לפני v2.1.208, קריאת הכלי עדיין יכלה להתקדם אם קטעתם במהלך callback ממתין של PreToolUse.

אם ה-callback צריך יותר זמן, הגדירו timeout גבוה יותר על ה-HookMatcher שלו. ב-TypeScript, השתמשו ב-AbortSignal מהארגומנט השלישי של ה-callback כדי לטפל בביטול בצורה מסודרת כשפסק הזמן מופעל.

#כלי נחסם באופן לא צפוי

  • בדקו את כל hooks של PreToolUse להחזרות של permissionDecision: 'deny'
  • הוסיפו לוגים ל-hooks כדי לראות איזה permissionDecisionReason הם מחזירים
  • ודאו שתבניות matcher אינן רחבות מדי: matcher ריק מתאים לכל הכלים

#קלט שהשתנה לא מוחל

  • ודאו ש-updatedInput נמצא בתוך hookSpecificOutput, לא ברמה העליונה:

    return {
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "allow",
        updatedInput: { command: "new command" }
      }
    };
  • אל תצמדו updatedInput עם permissionDecision: 'defer', שמשליך את הקלט שהשתנה. השמטת permissionDecision תקינה: הקלט שהשתנה עדיין חל דרך הערכת ההרשאות הרגילה. אפשר גם להחזיר 'allow' כדי לאשר אוטומטית את הקלט שהשתנה, או 'ask' כדי להציג אותו למשתמש לאישור

  • כללו את hookEventName ב-hookSpecificOutput כדי לזהות לאיזה סוג hook הפלט מיועד

#Hooks של סשן לא זמינים ב-Python

אפשר לרשום את SessionStart ואת SessionEnd כ-hooks מסוג callback של ה-SDK ב-TypeScript, אבל הם לא זמינים ב-SDK של Python כי טיפוס HookEvent שלו משמיט אותם. ב-Python הם זמינים רק כ-hooks של פקודת shell שמוגדרים בקובצי הגדרות כמו .claude/settings.json. אילו קובצי הגדרות אפליקציית ה-SDK טוענת תלוי ב-setting_sources או ב-settingSources. אם מגדירים את האפשרות הזאת, כללו את המקור שמחזיק את ה-hooks:

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

כדי להריץ לוגיקת אתחול כ-callback של ה-SDK של Python, השתמשו בהודעה הראשונה מ-client.receive_response() כטריגר.

#בקשות הרשאה של תת-סוכנים שמתרבות

כשמפעילים כמה תת-סוכנים, כל אחד עשוי לבקש הרשאות בנפרד לקריאות הכלים שלו. כדי להימנע מבקשות חוזרות, השתמשו ב-hooks של PreToolUse כדי לאשר אוטומטית כלים מסוימים, או הגדירו כללי הרשאה, שתת-סוכנים יורשים מהשיחה של ההורה.

#לולאות hook רקורסיביות עם תת-סוכנים

Hook של UserPromptSubmit שמפעיל תת-סוכנים יכול ליצור לולאות אינסופיות אם תת-הסוכנים האלה מפעילים את אותו hook. כדי למנוע זאת:

  • השתמשו במשתנה משותף או במצב סשן כדי לעקוב אם אתם כבר בתוך תת-סוכן
  • תחמו hooks כך שירוצו רק עבור סשן הסוכן ברמה העליונה

#systemMessage לא מופיע בפלט

השדה systemMessage מציג הודעה למשתמש, לא למודל. ב-Claude Code v2.1.227 ואילך, systemMessage של hook יכול להופיע בזרם ההודעות כ-SDKInformationalMessage. אם זה קורה תלוי באירוע. הסעיף של כל אירוע בדף ה-hooks אומר איך הפלט מוצג. כדי להעביר הקשר למודל, החזירו additionalContext.

לפני v2.1.227, ה-SDK הציג פלט של hook בזרם ההודעות רק עבור hooks של SessionStart ו-Setup. לכל אירוע אחר, הפלט הופיע רק באירועי מחזור החיים ש-includeHookEvents (include_hook_events ב-Python) מוסיף. הרשומה של האפשרות הזאת מכסה אילו אירועי מחזור חיים כל אירוע hook מייצר.

אם צריך להציג החלטות hook לאפליקציה באופן אמין, רשמו אותן בנפרד או השתמשו בערוץ פלט ייעודי.

#משאבים קשורים