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

תיעוד 145

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

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

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

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

#כיצד hooks עובדים

  1. אירוע מופעל: משהו קורה במהלך ביצוע הסוכן וה-SDK מפעיל אירוע: כלי עומד להיקרא (PreToolUse), כלי החזיר תוצאה (PostToolUse), תת-סוכן התחיל או עצר, הסוכן במצב idle, או שהביצוע הסתיים. ראה את רשימת האירועים המלאה.
  2. ה-SDK אוסף hooks רשומים: ה-SDK בודק אילו hooks רשומים עבור סוג אירוע זה. זה כולל hooks מסוג callback שאתה מעביר ב-options.hooks ו-hooks של פקודות shell מקובצי הגדרות כאשר הרשומה המתאימה של settingSources או setting_sources מופעלת, כפי שהיא מופעלת כברירת מחדל באפשרויות של query().
  3. התאמות (Matchers) מסננות אילו hooks ירוצו: אם ל-hook יש תבנית matcher (כמו "Write|Edit"), ה-SDK בודק אותה מול יעד האירוע (לדוגמה, שם הכלי). Hooks ללא matcher רצים עבור כל אירוע מסוג זה.
  4. פונקציות callback מתבצעות: פונקציית ה-callback של כל hook תואם מקבלת קלט על מה שמתרחש: שם הכלי, הארגומנטים שלו, ה-session ID, ופרטים נוספים הספציפיים לאירוע.
  5. ה-callback שלך מחזיר החלטה: לאחר ביצוע פעולות כלשהן (רישום ביומן, קריאות API, אימות), ה-callback שלך מחזיר אובייקט פלט שאומר לסוכן מה לעשות: לאפשר את הפעולה, לחסום אותה, לשנות את הקלט, או להזריק הקשר לתוך השיחה.

הדוגמה הבאה מחברת את השלבים הללו יחד. היא רושמת hook מסוג PreToolUse (שלב 1) עם matcher של "Write|Edit" (שלב 3) כך שה-callback יופעל רק עבור כלים שכותבים לקבצים. כאשר הוא מופעל, ה-callback מקבל את הקלט של הכלי (שלב 4), בודק אם נתיב הקובץ מכוון לקובץ .env, ומחזיר "permissionDecision": "deny" כדי לחסום את הפעולה (שלב 5):

Python:

import asyncio
from claude_agent_sdk import (
    AssistantMessage,
    ClaudeSDKClient,
    ClaudeAgentOptions,
    HookMatcher,
    ResultMessage,
)


# Define a hook callback that receives tool call details
async def protect_env_files(input_data, tool_use_id, context):
    
# Extract the file path from the tool's input arguments
    file_path = input_data["tool_input"].get("file_path", "")
    file_name = file_path.split("/")[-1]

    
# Block the operation if targeting a .env file
    if file_name == ".env":
        return {
            "hookSpecificOutput": {
                "hookEventName": input_data["hook_event_name"],
                "permissionDecision": "deny",
                "permissionDecisionReason": "Cannot modify .env files",
            }
        }

    
# Return empty object to allow the operation
    return {}


async def main():
    options = ClaudeAgentOptions(
        hooks={
            
# Register the hook for PreToolUse events
            
# The matcher filters to only Write and Edit tool calls
            "PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[protect_env_files])]
        }
    )

    async with ClaudeSDKClient(options=options) as client:
        await client.query("Create a .env file with the standard local development database configuration")
        async for message in client.receive_response():
            
# Filter for assistant and result messages
            if isinstance(message, (AssistantMessage, ResultMessage)):
                print(message)


asyncio.run(main())

TypeScript:

import { query, HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";

// Define a hook callback with the HookCallback type
const protectEnvFiles: HookCallback = async (input, toolUseID, { signal }) => {
  // Cast input to the specific hook type for type safety
  const preInput = input as PreToolUseHookInput;

  // Cast tool_input to access its properties (typed as unknown in the SDK)
  const toolInput = preInput.tool_input as Record<string, unknown>;
  const filePath = toolInput?.file_path as string;
  const fileName = filePath?.split("/").pop();

  // Block the operation if targeting a .env file
  if (fileName === ".env") {
    return {
      hookSpecificOutput: {
        hookEventName: preInput.hook_event_name,
        permissionDecision: "deny",
        permissionDecisionReason: "Cannot modify .env files"
      }
    };
  }

  // Return empty object to allow the operation
  return {};
};

for await (const message of query({
  prompt: "Create a .env file with the standard local development database configuration",
  options: {
    hooks: {
      // Register the hook for PreToolUse events
      // The matcher filters to only Write and Edit tool calls
      PreToolUse: [{ matcher: "Write|Edit", hooks: [protectEnvFiles] }]
    }
  }
})) {
  // Filter for assistant and result messages
  if (message.type === "assistant" || message.type === "result") {
    console.log(message);
  }
}

כאשר אתה מריץ כל אחד מהסקריפטים, Claude מנסה ליצור את הקובץ .env, ה-hook דוחה את הקריאה לכלי, והתגובה הסופית של Claude מסבירה שהוא אינו יכול ליצור קובצי .env.

#Hooks זמינים

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

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

#הגדרת תצורה של hooks

כדי להגדיר תצורה של hook, העבר אותו בשדה hooks של אפשרויות הסוכן שלך (ClaudeAgentOptions ב-Python, אובייקט ה-options ב-TypeScript). קטע קוד זה מניח שכבר הגדרת callback של hook, כמו protect_env_files ב-Python או protectEnvFiles ב-TypeScript מהדוגמה לעיל:

Python:

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)

TypeScript:

for await (const message of query({
  prompt: "Your prompt",
  options: {
    hooks: {
      PreToolUse: [{ matcher: "Bash", hooks: [myCallback] }]
    }
  }
})) {
  console.log(message);
}

האפשרות hooks היא מילון ב-Python או אובייקט ב-TypeScript, כאשר:

#Matchers

השתמש ב-matchers כדי לסנן מתי פונקציות ה-callback שלך יופעלו. השדה 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.
hooksHookCallback[]-חובה. מערך של פונקציות callback לביצוע כאשר התבנית מתאימה
timeoutnumberundefinedפסק זמן בשניות. כאשר מושמט, Claude Code מחיל את פסק הזמן המוגדר כברירת מחדל של האירוע. פונקציות ה-callback שלך ב-SDK פועלות לפי ברירות המחדל של hook מסוג command

השתמש בתבנית matcher כדי למקד לכלים ספציפיים בכל עת שניתן. Matcher עם 'Bash' רץ רק עבור פקודות Bash, בעוד שהשמטת התבנית מריצה את פונקציות ה-callback שלך עבור כל התרחשות של האירוע. השמט אותו בכוונה כדי לתעד ביומן כל קריאה לכלי שה-session שלך מבצע.

#פונקציות Callback

#קלטים (Inputs)

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

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

#פלטים (Outputs)

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

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

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

הערה: כאשר חלים מספר hooks או כללי הרשאות, ל-deny יש עדיפות על פני defer, שלו יש עדיפות על פני ask, שלו יש עדיפות על פני allow. אם hook כלשהו מחזיר deny, הפעולה נחסמת ללא קשר ל-hooks האחרים.

#פלט אסינכרוני (Asynchronous output)

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

Python:

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}

TypeScript:

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פסק זמן אופציונלי במילישניות עבור פעולת הרקע

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

#דוגמאות

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

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

דוגמה זו מיירטת קריאות לכלי Write ומשכתבת את הארגומנט file_path כדי להוסיף בתחילתו את /sandbox, ובכך מנתבת מחדש את כל כתיבות הקבצים לתיקיית sandbox. ה-callback מחזיר את updatedInput עם הנתיב ששונה ו-permissionDecision: 'allow' כדי לאשר אוטומטית את הפעולה המשוכתבת:

Python:

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

TypeScript:

const redirectToSandbox: HookCallback = async (input, toolUseID, { signal }) => {
  if (input.hook_event_name !== "PreToolUse") return {};

  const preInput = input as PreToolUseHookInput;
  const toolInput = preInput.tool_input as Record<string, unknown>;
  if (preInput.tool_name === "Write") {
    const originalPath = toolInput.file_path as string;
    return {
      hookSpecificOutput: {
        hookEventName: preInput.hook_event_name,
        permissionDecision: "allow",
        updatedInput: {
          ...toolInput,
          file_path: `/sandbox${originalPath}`
        }
      }
    };
  }
  return {};
};

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

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

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

דוגמה זו חוסמת כתיבה לתיקייה /etc ומסבירה מדוע הן למודל והן למשתמש:

  • permissionDecision: 'deny' עוצר את הקריאה לכלי.
  • permissionDecisionReason מסביר למודל מדוע, כדי שימנע מניסיונות חוזרים.
  • systemMessage מציג למשתמש מה קרה.

Python:

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

TypeScript:

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

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

כברירת מחדל, הסוכן עשוי לבקש הרשאה לפני שימוש בכלים מסוימים. דוגמה זו מאשרת אוטומטית כלים לקריאה בלבד במערכת הקבצים (Read, Glob, Grep) על ידי החזרת permissionDecision: 'allow', מה שמאפשר להם לרוץ ללא אישור משתמש בעוד שכל שאר הכלים נותרים כפופים לבדיקות הרשאות רגילות:

Python:

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

TypeScript:

const autoApproveReadOnly: HookCallback = async (input, toolUseID, { signal }) => {
  if (input.hook_event_name !== "PreToolUse") return {};

  const preInput = input as PreToolUseHookInput;
  const readOnlyTools = ["Read", "Glob", "Grep"];
  if (readOnlyTools.includes(preInput.tool_name)) {
    return {
      hookSpecificOutput: {
        hookEventName: preInput.hook_event_name,
        permissionDecision: "allow",
        permissionDecisionReason: "Read-only tool auto-approved"
      }
    };
  }
  return {};
};

#רישום מספר hooks במקביל

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

הדוגמה להלן רושמת שלוש בדיקות עצמאיות עבור כל קריאה לכלי:

Python:

options = ClaudeAgentOptions(
    hooks={
        "PreToolUse": [
            HookMatcher(hooks=[authorization_check]),
            HookMatcher(hooks=[input_validator]),
            HookMatcher(hooks=[audit_logger]),
        ]
    }
)

TypeScript:

const options = {
  hooks: {
    PreToolUse: [
      { hooks: [authorizationCheck] },
      { hooks: [inputValidator] },
      { hooks: [auditLogger] }
    ]
  }
};

#סינון באמצעות matchers מרובי-כלים

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

  • רשימה מדויקת מופרדת בתו קו אנכי (Write|Edit|NotebookEdit) מפעילה את file_security_hook רק עבור כלים לשינוי קבצים.
  • ביטוי רגולרי (^mcp__) מפעיל את mcp_audit_hook עבור כל כלי MCP ששמו מתחיל ב-mcp__.
  • השמטת matcher מפעילה את global_logger עבור כל קריאה לכלי ללא קשר לשמו.

Python:

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]),
        ]
    }
)

TypeScript:

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

#מעקב אחר פעילות תת-סוכן

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

Python:

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

TypeScript:

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

#ביצוע בקשות HTTP מתוך hooks

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

דוגמה זו שולחת webhook לאחר שכל כלי מסיים, ומתעדת איזה כלי רץ ומתי. ה-hook תופס שגיאות כך ש-webhook שנכשל אינו מפריע לסוכן:

Python:

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

TypeScript:

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

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

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

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

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

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

כל התראה כוללת שדה message עם תיאור קריא לבני אדם ובאופן אופציונלי title.

דוגמה זו מעבירה כל התראה לערוץ Slack. היא דורשת כתובת Slack incoming webhook, אותה אתה יוצר על ידי הוספת אפליקציה לסביבת העבודה שלך ב-Slack והפעלת incoming webhooks:

Python:

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())

TypeScript:

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 מכיוון שה-session מסתיים לפני ש-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

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

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

  • PreToolUse: קלוד קוד אינו מריץ את הקריאה לכלי, Claude מקבל תוצאת כלי המציינת שה-hook לא הגיב לפני פסק הזמן שלו, והתור נמשך. אם hook אחר מסוג PreToolUse החזיר סירוב מפורש, Claude מקבל את הדחייה הזו במקום את שגיאת פסק הזמן. לפני גרסה v2.1.210, קלוד קוד דיווח על פסק הזמן ל-Claude כדחייה של המשתמש, מה שגרם ל-sessions ללא השגחה לעצור ולהמתין לקלט.
  • PostToolUse ו-PostToolUseFailure: קלוד קוד שומר את תוצאת הכלי והתור נמשך.
  • UserPromptSubmit ו-UserPromptExpansion: קלוד קוד חוסם את ה-prompt עם הודעה המציינת את שם ה-hook ואת פסק הזמן, וה-session נמשך. מכיוון ש-callback באירועים אלו יכול לשמש כשער מדיניות, קלוד קוד לעולם אינו מאפשר ל-prompt שחווה פסק זמן לעבור ללא בדיקה. לפני גרסה v2.1.208, קלוד קוד סיים את השאילתה עם error_during_execution כאשר callback באירועים אלו חווה פסק זמן.
  • Stop ו-SubagentStop: קלוד קוד מציג אזהרה והסוכן עוצר כרגיל.
  • PreModelSwitch: קלוד קוד חוסם את המעבר בין המודלים. Hook שאינו עונה לא אישר את המעבר.
  • אירועים אחרים, כגון Notification, PreCompact, ו-PostModelSwitch: קלוד קוד רושם את הכשל ביומן וממשיך.

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

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

Python:

options = ClaudeAgentOptions(
    setting_sources=["project"],  
# Loads .claude/settings.json including hooks
)

TypeScript:

const options = {
  settingSources: ["project"] // Loads .claude/settings.json including hooks
};

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

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

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

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

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

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

#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.

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

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