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

תיעוד 79

להגיב לאירועים עם מוד

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

הוק הוא מטפל באירוע: פונקציה ש Claude Code מריץ כשמתרחש אירוע בעל שם. Claude Code יורה אירוע בכל נקודה שבה הוא עומד לפעול, למשל כשהוא מריץ כלי, שולח פרומפט, שולח בקשה למודל, או מתחיל או מסיים סשן. ההוק שלכם רץ לפני ש Claude Code פועל, ולכן הוא יכול לצפות באירוע, לשכתב אותו, או לענות עליו במקום Claude Code. רושמים הוק עם on(eventName, handler).

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

#איך הוק מטפל באירוע

הוק יושב בין אירוע לבין מה ש Claude Code היה עושה לגביו, ולכן הוא יכול לצפות באירוע, לשכתב אותו, או לענות עליו בעצמו. הוא מקבל שלושה ארגומנטים: את ה API של המודים בתור $, את האירוע בתור e, ואת המטפל הבא בתור next. המטפלים של אירוע יוצרים שרשרת middleware. next(e) קורא למטפל הבא, שהוא הוק של מוד אחר או, בסוף השרשרת, ההתנהגות של Claude Code עצמו, והוא נפתר לתוצאה. מה שההוק שלכם עושה עם next קובע איזו משלוש הפעולות הוא מבצע.

#לצפות באירוע

כדי לצפות באירוע בלי לשנות אותו, בצעו את העבודה שלכם והחזירו next(e). ההוק הזה רושם בלוג כל כלי ש Claude עומד להשתמש בו:

on('tool.call', async ($, e, next) => {
  // Runs before the tool does
  $.ui.log('Claude is about to use ' + e.tool)
  // Pass the event on unchanged
  return next(e)
})

לפני שכל כלי רץ, מופיעה בתמליל שורה עמומה כמו ● my-mod: Claude is about to use Bash, כאשר my-mod הוא שם התוסף שלכם. הכלי רץ כפי שהיה רץ בלי המוד.

כדי לפעול אחרי האירוע, await next(e), בצעו את העבודה, והחזירו את התוצאה. ההוק הזה רושם בלוג כל כלי אחרי שהוא רץ:

on('tool.call', async ($, e, next) => {
  // Let the tool run, and wait for its result
  const result = await next(e)
  // Runs after the tool does
  $.ui.log(e.tool + ' finished')
  // Give the result back unchanged
  return result
})

השורה מופיעה עכשיו אחרי שכל כלי מסתיים. Claude קורא את אותה תוצאה בכל מקרה, כי ההוק מחזיר את מה ש next(e) נפתר אליו.

#לשכתב אירוע

כדי לשנות את מה ש Claude Code פועל עליו, למשל את הטקסט של פרומפט, קראו ל next עם עותק מעודכן של האירוע. האירוע עצמו אינו ניתן לשינוי: הוא קפוא לעומק, והשמה לשדה זורקת. ההוק הזה מקצץ כל פרומפט לפני שהוא נשלח:

on('prompt.submit', async ($, e, next) => {
  // Pass on a copy of the event with its text changed
  return next({ ...e, text: e.text.trim() })
})

המטפלים המאוחרים ו Claude Code מקבלים את הפרומפט המקוצץ ולעולם לא רואים את המקור. אפשר גם לשנות את התוצאה: await next(e), ואז להחזיר עותק של התוצאה עם שדה שהוחלף.

#לענות על אירוע

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

on('tool.call', { tool: 'Bash' }, async () => {
  // No call to next, so the command never runs
  return { deny: 'Bash is turned off in this project. Use the file tools.' }
})

כש Claude מנסה פקודת Bash, הפקודה לא רצה, ו Claude קורא את טקסט ה deny כתוצאת הכלי. לכל אירוע יש צורת תוצאה משלו, ש הפניית האירועים מפרטת.

#לסנן אילו אירועים הוק מטפל בהם

כדי להריץ הוק רק לחלק מהאירועים, העבירו מסנן כארגומנט השני ל on. Claude Code קורא למסנן matcher. זה אובייקט שהשדות שלו מושווים לשדות האירוע, וההוק רץ רק כשכל שדה תואם. שדה יכול להיות ערך, מערך של ערכים מותרים, או ביטוי רגולרי.

כל שורה בדוגמה הזו רושמת את אותה פונקציה, hook, לקבוצה צרה יותר של קריאות כלים:

// A string matches one value: Bash calls only
on('tool.call', { tool: 'Bash' }, hook)
// An array matches any value in it: Edit calls and Write calls
on('tool.call', { tool: ['Edit', 'Write'] }, hook)
// A regular expression matches by pattern: every tool of one MCP server
on('tool.call', { tool: /^mcp__github__/ }, hook)

hook רץ פעם אחת לקריאת Bash, Edit או Write, ופעם אחת לקריאה לכלי שהשם שלו מתחיל ב mcp__github__. קריאה לכל כלי אחר, למשל Read, לא תואמת לאף אחת משלוש, ולכן hook לא רץ עבורה.

שם האירוע יכול להיות תו כללי. 'classic.*' תואם כל אירוע הוק של הגדרות. '*' תואם כל אירוע חוץ מ אירועי הטלמטריה, שמקבלים שם משלהם ומסנן { to: 'collector' }.

רשמו כל אירוע פעם אחת לכל matcher. אם קוראים ל on פעמיים עבור session.start בלי matcher, המודול נכשל בטעינה עם on("session.start") is registered twice without a matcher. שימו את כל מה שהמוד שלכם עושה בתחילת הסשן בהוק אחד.

#הוק על מה ש Claude עושה

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

#להגן על קריאת כלי או לשנות אותה

הוק tool.call רואה כל כלי ש Claude עומד להשתמש בו, ולכן הוא יכול לסרב לקריאה, לשנות את הארגומנטים שלה, או להעביר אותה. tool.call נורה כש Claude Code עומד להריץ כלי, כולל קריאות שסוכן משנה מבצע וקריאות לכלי MCP. e.tool הוא שם הכלי והארגומנטים של הכלי הם שדות של e, למשל e.command עבור Bash. כשקוראים ל next(e), Claude Code מריץ את בדיקת ההרשאה ואז את הכלי.

ההוק הזה מסרב לפקודת Bash שעושה force-push, ואומר ל Claude למה:

// The matcher limits the hook to Bash calls, so e.command is the shell command
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
  if (/git push .*--force/.test(e.command)) {
    // Returning without calling next answers the event, so the command never runs
    return { deny: 'Force pushes are not allowed in this repository. Push to a new branch instead.' }
  }
  // Every other command goes on to the permission check and then to Bash
  return next(e)
})

כש Claude מנסה git push --force, הפקודה לא רצה ולא מופיעה בקשת הרשאה, כי ההוק אף פעם לא קורא ל next. Claude קורא את טקסט ה deny כתוצאת הכלי, אז כתבו אותו כהוראה ש Claude יכול לפעול לפיה. כל פקודת Bash אחרת רצה כפי שהייתה רצה בלי המוד.

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

כדי לפעול אחרי שכלי רץ, await next(e), בצעו את העבודה, והחזירו את מה ש next נתן לכם. ההוק הזה רושם בלוג כל קובץ .mdx ש Claude משנה, עם $.ui.log, שמוסיף שורה עמומה לתמליל ש Claude לא קורא:

on('tool.call', { tool: ['Edit', 'Write'] }, async ($, e, next) => {
  // Wait for the permission check and the tool, and keep what they produced
  const result = await next(e)
  // A refused call comes back as { deny }, and a failed one has isError set
  const changed = !result.deny && !result.isError
  if (changed && e.file_path.endsWith('.mdx')) $.ui.log('Claude changed ' + e.file_path)
  // Return the result as it came, so Claude reads what the tool returned
  return result
})

אחרי ש Claude עורך או כותב קובץ .mdx, שורה עמומה בתמליל מציינת את הקובץ. לא נרשם דבר עבור סוג אחר של קובץ, או עבור קריאה שסורבה או נכשלה. המבט של Claude על הקריאה לא משתנה, כי ההוק מחזיר את התוצאה שקיבל.

ההוק שלכם יכול גם לשנות קריאה, לנסות אותה שוב, לענות עליה בעצמו, או למנוע את התוצאה שלה:

  • לשנות את הקריאה: העבירו ארגומנטים ששונו ל next.
  • לנסות את הקריאה שוב: קראו ל next(e) שוב. הוק שרואה isError בתוצאה הראשונה יכול להריץ את הכלי פעם שנייה ולהחזיר את התוצאה הזו.
  • לענות על הקריאה בעצמכם: החזירו אובייקט עם שדה result, בלי לקרוא ל next, ולכלי מובנה תנו ל result את הצורה שיש לתוצאה של הכלי עצמו ב טיפוסים של הבילד שלכם. לא מופיעה בקשת הרשאה והכלי לא רץ, ולכן התוצאה שאתם מחזירים היא כל מה ש Claude לומד על מה שקרה.
  • למנוע את התוצאה מ Claude: החזירו { deny: reason } אחרי await next(e). Claude קורא את הסיבה שלכם במקום מה ש next החזיר. כשהכלי רץ, ה deny מונע את התוצאה שלו מ Claude ולא מבטל דבר שהכלי עשה. כשהכלי רץ והצליח, הסיבה באה אחרי הערה כמו Bash ran, and a plugin withheld its result:.

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

#להחזיק קריאת כלי עד שהמשתמש מחליט

הוק יכול להשהות קריאת כלי ולשאול את המשתמש מה לעשות לפני שהיא ממשיכה. הוק tool.call יכול לעשות await לפני שהוא קורא ל next או מחזיר, וקריאת הכלי נשארת ממתינה עד אז. כדי להציג את השאלה למשתמש, קראו ל $.ui.ask. זה מציג את השאלה שלכם מעל רשימה ממוספרת של האפשרויות שלכם, בדיאלוג ש Claude משתמש בו כדי לשאול אתכם משהו, ונפתר לתווית שהמשתמש בוחר. אחרי האפשרויות שלכם, הדיאלוג מוסיף שורה להקלדת תשובה אחרת ושורה Chat about this.

התבנית RISKY בדוגמה הזו תואמת את rm -r, rm -rf, git reset --hard, ו git push עם --force, ומפספסת איותים אחרים כמו git push -f. המודול הזה שואל לפני שהוא מריץ פקודת Bash שתואמת את התבנית:

const RISKY = /\brm\s+-rf?\b|\bgit\s+reset\s+--hard\b|\bgit\s+push\b.*--force/

export function register(on) {
  on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
    // Let every other command through without a question
    if (!RISKY.test(e.command)) return next(e)
    // Start from the safe answer, so a question nobody answers refuses the command
    let answer = 'Refuse'
    try {
      // The tool call waits here until the user picks one of the two labels
      answer = await $.ui.ask('Run this command? ' + e.command, ['Run it', 'Refuse'])
    } catch {
      // The user dismissed the question, or this is a claude -p run with nobody to ask
    }
    if (answer !== 'Run it') {
      // Answer without calling next, so the command doesn't run
      return { deny: 'The user declined this command. Ask before trying a different approach.' }
    }
    return next(e)
  })
}

כש Claude מנסה פקודה כמו rm -rf build, השאלה מופיעה עם הפקודה בתוכה, והפקודה ממתינה לתשובה:

  • המשתמש בוחר Run it: ההוק קורא ל next(e), ובדיקת ההרשאה הרגילה עדיין רצה אחריו
  • המשתמש בוחר Refuse: הפקודה לא רצה, ו Claude קורא את טקסט ה deny
  • המשתמש מקליד תשובה: $.ui.ask נפתר לטקסט שהוקלד. ההוק משווה אותו ל Run it, ולכן כל טקסט אחר מסרב לפקודה.
  • אף אחד לא עונה: $.ui.ask נדחה כשהמשתמש סוגר את השאלה או בוחר Chat about this, וגם בהרצה של claude -p, ולכן בלוק ה catch משאיר את התשובה על Refuse

החזיקו את ההמתנה בתוך קריאת API של מודים כמו $.ui.ask, כי הזמן הזה לא נספר מול מגבלת הזמן של ההוק. זמן שמבלים ב await על promise משלכם כן נספר.

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

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

כדי להחליט אם קריאת כלי רשאית לרוץ, טפלו ב tool.check, האירוע שבו Claude Code מקבל את ההחלטה הזו.

עבור פקודה או נתיב קבועים, השתמשו ב כלל הרשאה כמו Bash(npm test), שלא דורש קוד. טפלו ב tool.check כשההחלטה תלויה במה שנכון באותו רגע, למשל ענף ה Git הנוכחי או ערך שהוק אחר רשם.

  • מה להחזיר: כדי להחליט את הקריאה בעצמכם, החזירו אובייקט משלכם, למשל { decision: 'allow' } או { decision: 'deny', reason }. כדי להשאיר את ההחלטה כמו שהיא, החזירו את מה ש await next(e) נותן לכם.
  • מה await next(e) נותן לכם: ההחלטה של Claude Code עצמו, כאובייקט שה decision שלו הוא allow, ask, או deny. לא מופיעה בקשת הרשאה בזמן ש next(e) רץ.
  • ארגומנטים: e.input מחזיק את הארגומנטים של הכלי, למשל command עבור Bash
  • מתי tool.check נורה: אחרי שהוקי PreToolUse רצים, ולפני בקשת ההרשאה, המסווג של מצב auto, או הוק PermissionRequest

ההוק הזה מסרב ל git push כל עוד הענף הנוכחי הוא main:

on('tool.check', { tool: 'Bash' }, async ($, e, next) => {
  // Claude Code's own decision
  const decided = await next(e)
  if (!e.input.command.includes('git push')) return decided
  const branch = await $.process.run(['git', 'branch', '--show-current'])
  if (branch.stdout.trim() !== 'main') return decided
  return { decision: 'deny', reason: 'Push from a branch other than main' }
})

ההוק דוחה git push על main ומעביר כל קריאה אחרת הלאה:

  • על main: ההוק מחזיר deny, גם כשכלל מתיר git push
  • על ענף אחר, ולפקודות אחרות: הקריאה מקבלת את ההחלטה שהייתה מקבלת בלי המוד
  • ההוק זורק או חורג מזמן: הקריאה גם מקבלת את ההחלטה שהייתה מקבלת בלי המוד. כדי לסרב לקריאה במקום זאת, הוסיפו מטפל .catch שמחזיר { decision: 'deny', reason: 'the reason' }.

ההוק מתאים את הטקסט של הפקודה, אז התייחסו אליו כאל תזכורת ל Claude. כדי לחסום דחיפות ל main לכולם, הגנו על הענף במארח ה Git שלכם.

[!NOTE]

הוק tool.check יכול גם להחזיר allow או ask. ב מצב plan, עבור קריאה שאינה לקריאה בלבד, למשל עריכת קובץ, ההוק שלכם יכול להחמיר את ההחלטה ש next(e) מחזיר אבל לא יכול לרכך אותה. עבור כללי ההרשאה והחלטות הוק PreToolUse ש allow של ההוק שלכם לא יכול להחליף, ראו הרחבת הרשאות עם הוקים.

#לבדוק אם קריאת כלי היא לקריאה בלבד

בהוק tool.check, e.isReadOnly הוא true כשהכלי מדווח שהקריאה היא לקריאה בלבד. אחרת השדה נעדר, לא false. e.isReadOnly דורש Claude Code v2.1.304 או מאוחר יותר.

חלק מהכלים, כמו Read, Grep, Glob ו WebSearch, מדווחים על כל קריאה כלקריאה בלבד. כלי Bash וכלי MCP מדווחים על קריאה כלקריאה בלבד במקרים האלה:

הערך הוא הדיווח של הכלי עצמו, אז אל תאשרו קריאה על סמך השדה הזה לבדו. למשל, קריאות WebFetch ו Agent מדווחות כלקריאה בלבד, אף על פי ש WebFetch מבקש URL ו Agent מתחיל סוכן משנה שמריץ כלים משלו.

ההוק הזה מחזיר ask לכל קריאת mcp__github__ שאינה לקריאה בלבד ושלא כבר נדחתה:

on('tool.check', { tool: /^mcp__github__/ }, async ($, e, next) => {
  const decided = await next(e)
  // Read-only calls and denied calls pass through unchanged
  if (e.isReadOnly || decided.decision === 'deny') return decided
  return { decision: 'ask', reason: 'This tool can change the GitHub repository' }
})

#לסנן הוק tool.check לפי אם הקריאה היא לקריאה בלבד

כדי להגביל אילו קריאות ההוק שלכם מטפל בהן לפי e.isReadOnly, השתמשו ב matcher לקריאות לקריאה בלבד ובגוף ההוק לשאר:

  • קריאות לקריאה בלבד בלבד: הוסיפו isReadOnly: true ל matcher, כמו ב on('tool.check', { tool: 'Bash', isReadOnly: true }, hook)
  • קריאות שאינן לקריאה בלבד: בדקו את e.isReadOnly בגוף ההוק, כמו ש דוגמת mcp__github__ עושה. אם שמים isReadOnly: false ב matcher, ההוק שלכם אף פעם לא רץ.

#שכתוב או הוספה להנחיה

הוק prompt.submit רואה כל הנחיה לפני שהתור מתחיל, ולכן הוא יכול לשכתב את הטקסט או להוסיף אליו. e.text הוא מה שהוקלד.

כדי לעשות את זההחזר את זה
שכתוב את ההנחיה. התמליל והיסטוריית ההנחיות שלך מציגים את הטקסט החדש.next({ ...e, text: newText })
הוספת טקסט שרק Claude קורא, אחרי ההנחיהnext({ ...e, context: [...(e.context ?? []), extraText] })
עצירת שליחת ההנחיה{ drop: 'the reason' }

ההוק הזה מוסיף את שם הענף הנוכחי עבור Claude בכל פעם שהנחיה מזכירה pull request:

on('prompt.submit', async ($, e, next) => {
  // העבר הלאה הנחיה שלא מזכירה pull request כמו שהיא
  if (!/\bPR\b|pull request/i.test(e.text)) return next(e)
  const git = await $.process.run(['git', 'branch', '--show-current'])
  // מחוץ למאגר git הפקודה נכשלת, ולכן אין ענף להוסיף
  if (git.exitCode !== 0) return next(e)
  // שמור כל הקשר שהוק קודם הוסיף, והוסף שורה נוספת עבור Claude
  return next({ ...e, context: [...(e.context ?? []), 'Current branch: ' + git.stdout.trim()] })
})

כששולחים הנחיה כמו open a PR for this change, ההודעה נראית אותו דבר בתמליל, וגם Claude קורא אחריה שורה כמו Current branch: feature/auth. הנחיה שלא מזכירה pull request עוברת בלי שינוי, ו-git לא רץ.

כדי לעצור הנחיה, החזר { drop: 'the reason' } בלי לקרוא ל-next. הטקסט חוזר לשדה הקלט של ההנחיה של המשתמש, והם רואים Prompt dropped by a hook: ואחריו הסיבה שלך, ולכן פנה בסיבה אליהם. ההנחיה עדיין עוברת במקרים האלה:

  • ההוק שלך מחזיר drop אחרי שקריאה ל-next(e) שלו כבר העבירה את ההנחיה: התור עדיין רץ, וההוק נכשל עם הודעה שכוללת a drop after its next() was answered
  • ההוק שלך זורק או שפג הזמן שלו: Claude Code מדלג עליו ושולח את ההנחיה. הוסף מטפל .catch כדי שכשל לא ישלח הנחיה שההוק היה עוצר.

אירועים אחרים מכסים את שאר מה ש-Claude קורא: prompt.section לכל מקטע של הנחיית המערכת, prompt.context להקשר שנשלח עם ההודעה הראשונה, ו-skill.prompt לטקסט של מיומנות. טקסט מהוקים האלה שמשתנה בין בקשות מבטל את מטמון ההנחיה. בהוק skill.prompt אפשר גם לקרוא מאיזה תוסף או קובץ המיומנות הגיעה.

#מעקב אחרי תור

תור הוא כל מה ש-Claude עושה בתשובה להנחיה אחת. טפל ב-turn.start, ב-turn.step וב-turn.complete כדי לעקוב אחרי תור אחד:

אירועמתי הוא נורהמה הוק יכול לעשות
turn.startתור מתחיללצפות. e.turnId מזהה את התור בשני האירועים האחרים.
turn.stepClaude Code עומד לשלוח בקשה אחת למודל. לתור עם קריאות לכלים יש כמה. e.agentId מוגדר לבקשה של תת סוכן.לקרוא את שימוש הטוקנים של כל בקשה, לשלוח אותה למודל אחר עם next({ ...e, model }), או לענות בלי לקרוא למודל
turn.completeהתור הסתיים, כולל תור שהמשתמש קטע, שבו e.isAborted הוא true. e.answer הוא הטקסט הסופי של Claude, e.durationMs כמה זמן זה לקח, ו-e.usage סיכומי הטוקנים של התור. תור של תת סוכן יורה אותו כש-e.agentId מוגדר.לצפות, או להחזיר אובייקט עם שדה text, כמו { text: 'Done in 12 seconds' }, כדי להציג שורה מתחת לתשובה

כתוב הוק turn.step כמחולל אסינכרוני, כי האירוע זורם. yield* next(e) מעביר את התשובה תוך כדי הזרימה ומוערך לתוצאה הסופית. ההוק הזה מתעד כמה מכל בקשה ה-API של Claude הגיש ממטמון ההנחיה:

// function* הופך את ההוק למחולל, שיכול להעביר את התשובה חלק אחר חלק
on('turn.step', async function* ($, e, next) {
  // שלח את הבקשה, העבר כל חלק כשהוא מגיע, ושמור את התוצאה הסופית
  const result = yield* next(e)
  // דלג על תוצאה שלא מדווחת ספירות טוקנים
  if (result.usage) {
    $.ui.log('cache read ' + result.usage.cache_read_input_tokens + ' · wrote ' + result.usage.cache_creation_input_tokens)
  }
  // החזר את התוצאה בלי שינוי, כדי שהתור ימשיך כרגיל
  return result
})

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

result.usage מחזיק את ספירות הטוקנים שה-API של Claude מדווח לבקשה, וגם את ה-model שענה: input_tokens, output_tokens, cache_read_input_tokens ו-cache_creation_input_tokens. ההוק רץ גם לבקשות של תת סוכנים, אז בדוק את e.agentId כשרוצים רק את השיחה הראשית.

כדי לראות את קריאות הכלים שה-API הריץ בעצמו במהלך הבקשה, כמו קריאות לכלי advisor, קרא את result.serverToolUses. Claude Code לא מריץ את הקריאות האלה, ולכן שום הוק tool.call או tool.check לא נורה עבורן. השדה נעדר כשלתשובה אין קריאות כאלה, והוא דורש את Claude Code מגרסה v2.1.290 ואילך.

#שליחת בקשה למודל אחר

אם בקשת turn.step נכשלת עם שגיאת מודל כש-model הוא כינוי כמו haiku, עדכן ל-Claude Code מגרסה v2.1.309 ואילך, או השתמש במזהה מודל מלא. לפני v2.1.309, כינוי יצא כמו שנכתב.

כדי לשלוח בקשה למודל אחר, העבר ל-next את האירוע עם model ששונה, כמו ב-yield* next({ ...e, model: 'haiku' }).

#שינוי או סירוב לקריאת הגדרות

השתמש בהוק settings.read כדי לשנות או לסרב לקריאת הגדרות, של מוד או של Claude Code עצמו:

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

בדוק אילו מודים מקבלים את האירוע הזה ואת גרסת Claude Code שלך:

  • השומר המובנה: עם הגדרות מנוהלות, או עם /login של Team או Enterprise כאישור הסשן, השומר המובנה יכול לדלג על ההוק הזה במוד שמתקינים או טוענים עם --plugin-dir. כדי לבדוק, חפש ביומן ניפוי השגיאות את settings.read bypassed by cc-plugin-sec-default. אם שורה עם הטקסט הזה מציינת את המוד שלך, בקש ממנהל הארגון להתקין את המוד כאחד מהמודים של הארגון.
  • הגרסה: ירי של האירוע בקריאה של Claude Code עצמו, ושדה e.layers, דורשים את Claude Code מגרסה v2.1.304 ואילך.

#מה הוק settings.read מקבל

שדהסוגמה הוא מחזיק
e.layersarrayאובייקט { source, settings } אחד לכל מקור בשימוש שמחזיק הגדרות, הקדימות הנמוכה ביותר ראשונה: plugin, user, project, local, flag, policy. ריק ב-claude plugin test.
e.sourcestringהמקור האחד שמוד ביקש, כמו ב-$.settings.read({ source: 'policy' }). אז e.layers מחזיק את השכבה הזאת לבדה. נעדר בקריאה של Claude Code עצמו ובכל קריאה אחרת של ההגדרות הממוזגות.

ב-e.layers, plugin מחזיק הגדרות שתוספים מופעלים מספקים, והוא שם שכבה בלבד, לא source שאפשר להעביר ל-$.settings.read. flag מחזיק את ההגדרות שהועברו עם --settings, ו-policy מחזיק הגדרות מנוהלות.

#הבחנה בין קריאה של Claude Code עצמו לקריאה של מוד

בדוק את next.origin:

  • קריאה של Claude Code עצמו: { plugin: 'engine', tier: 'core' }
  • קריאה של מוד: plugin הוא שם המוד הקורא

#מה הוק settings.read יכול להחזיר

החזר את התוצאה של next(e) בלי שינוי כדי להשאיר את הקריאה כמו שהיא, או החזר אחד מאלה:

החזרהבקריאה של Claude Code עצמובקריאה של מוד
{ value }לכל מפתח ששונה, מחליפים את כל הערך שלו בהגדרות הממוזגות של הסשן, אם הערך שלך מתאים לסכמה של המפתחהקריאה של המוד הזה נפתרת ל-value שלך, או ל-key שהוא ביקש שנלקח מ-value שלך, אז החזר אובייקט שלם ולא מערך
{ deny: reason }הסשן שומר את ההגדרות כמו שמוזגו לפני שהוק כלשהו ענה, ויומן ניפוי השגיאות רושם את ה-reason שלךהקריאה של המוד הזה נדחית עם <calling mod>: $.settings.read: <reason>

בקריאה של Claude Code עצמו, בנה את ה-{ value } שלך מ-value של next(e), כי מפתח שמשמיטים לא מוגדר. במקום ש-Claude Code קורא מפתח כמו env מכל מקור בנפרד, לשינוי שלו ב-value שלך אין השפעה.

#מה next(e) נותן להוק settings.read

next(e) נפתר ל-{ value } או ל-{ deny }:

  • { value }: value הוא ההגדרות הממוזגות, או ההגדרות של מקור אחד כש-e.source מוגדר. אם הוק שרץ אחרי שלך החזיר { value } משלו, מקבלים את ה-value של ההוק ההוא במקום.
  • { deny }: הוק שרץ אחרי שלך סירב לקריאה. החזר את ה-{ deny } הזה בלי שינוי.

#שינוי הגדרה אחת מהוק settings.read

קודם בדוק אילו מודים מקבלים את האירוע הזה, ומאיזו גרסה.

כדי לשנות מפתח אחד, פזר את ה-value מ-next(e) והגדר את המפתח הזה, כמו שההוק הזה עושה עם spinnerTipsEnabled. ההוק לא בודק את next.origin, ולכן ההוק משנה את המפתח בקריאה של Claude Code עצמו ובקריאה ממוזגת של מוד כאחד:

on('settings.read', async ($, e, next) => {
  const read = await next(e)
  // העבר הלאה קריאה של מקור אחד, כדי שרק ההגדרות הממוזגות ישתנו.
  // העבר הלאה גם { deny } מהוק מאוחר יותר: אין לו value.
  if (e.source || !read.value) return read
  // שמור כל מפתח אחר ושנה את זה
  return { value: { ...read.value, spinnerTipsEnabled: false } }
})

בקריאה של Claude Code עצמו, אם ההוק שלך מחזיר ערך מחוץ לסכמה של המפתח, כמו מחרוזת, המפתח הזה לבדו שומר את הערך שמוזג לפני שהוק כלשהו ענה. יומן ניפוי השגיאות אומר אז the answer for <key> is no value the key can hold. אם הערך שלך מתאים והמפתח עדיין לא משתנה, ראה למה מפתח לא נכנס לתוקף.

#טיפול באירועי הוקי ההגדרות

הוקי הגדרות הם הוקי הפקודה, ה-HTTP, ההנחיה והסוכן שמגדירים בקובצי הגדרות. כל אירוע הוק הגדרות, כמו Stop, SessionEnd או PostToolUse, הוא גם אירוע בשם classic. ואחריו שם אירוע הוק ההגדרות, כמו classic.Stop. e הוא ה-JSON שהוק הגדרות מקבל ב-stdin, כולל transcript_path. במקום שהשומר המובנה נטען, הוק על האירועים האלה במוד שמתקינים לא רץ.

ההוק הזה משתמש ב-Stop, שנורה כש-Claude מסיים להגיב, כדי לתעד איפה התמליל של הסשן נשמר:

on('classic.Stop', async ($, e, next) => {
  // ל-e יש אותם שדות שהוק Stop בקובץ הגדרות קורא מ-stdin
  $.ui.log('Transcript saved at ' + e.transcript_path)
  // העבר את האירוע הלאה, כדי שהוקי Stop בקובצי ההגדרות שלך עדיין ירוצו
  return next(e)
})

בכל פעם ש-Claude מסיים להגיב, שורה עמומה בתמליל נותנת את הנתיב של קובץ התמליל. ההוק מחזיר next(e), ולכן הוא צופה באירוע ולא משנה דבר באופן שבו התור מסתיים.

#הרצה לצד מודים אחרים

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

#הסדר שבו מודים רצים

הוקים על אותו אירוע יוצרים שרשרת middleware אחת. ה-next של כל מוד קורא להוק של המוד הבא, וה-next האחרון מגיע להתנהגות של Claude Code עצמה. המוד הראשון הוא החיצוני ביותר: הוא רואה את האירוע לפני האחרים ואת התוצאה אחריהם, והוא מחליט אם האחרים רצים בכלל. מוד מאוחר יותר לא יכול למנוע ממוד מוקדם יותר לראות אירוע.

Claude Code מסדרת את השרשרת לפי המקור של כל מוד:

  1. השומר המובנה sec-default@builtin, מוד שמובנה ב-Claude Code וש-/plugin מציג אותו כ-cc-plugin-sec-default, במקום שבו הוא נטען, מודים שהארגון שלך מציין ב-prependPlugins, ואחר כך כל מוד אחר שנחשב למוד של הארגון שלך ואינו ב-appendPlugins
  2. מודים שאתה מתקין
  3. מודים שהארגון שלך מציין ב-appendPlugins
  4. מודים אחרים שמובנים ב-Claude Code

מבין המודים שאתה מתקין, מוד רץ לפני המודים שהוא מציין תחת dependencies במניפסט שלו. בתוך מודול אחד, הוקים רצים בסדר שבו register קרא ל-on.

#איפה הוקים של הגדרות רצים בסדר

הוקי PreToolUse שמוגדרים בקובצי הגדרות רצים גם במהלך קריאה לכלי, בנקודות קבועות בשרשרת המודים:

  • הוקי PreToolUse מהגדרות מנוהלות: רצים לפני הוק tool.call של המוד הראשון. כשהוק מנוהל חוסם את הקריאה, אף הוק tool.call של מוד לא רץ עבור אותה קריאה.
  • הוקי PreToolUse מכל קובץ הגדרות אחר ומ-hooks/hooks.json של תוספים: רצים אחרי שהמוד האחרון קורא ל-next, כחלק מההתנהגות של Claude Code עצמה. מוד שעונה על tool.call בלי לקרוא ל-next מונע מהם לרוץ, ומוד שקורא ל-next רואה את ההחלטה שלהם בתוצאה שהוא מחזיר.

tool.check מופעל אחרי שהוקי PreToolUse רצו. לכללי ההרשאות ולהחלטות של הוק PreToolUse ש-allow של הוק tool.check לא יכול להחליף, ראה הרחבת הרשאות עם הוקים.

#טיפול בהוק שנכשל

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

  • הוא נכשל לפני שקרא ל-next: Claude Code מדלגת עליו, והמטפל הבא רץ במקומו
  • הוא נכשל אחרי ש-next הושלם: התוצאה הזו נשארת בתוקף, ושום דבר לא רץ פעם שנייה

שורה אחת מציינת את המוד, את האירוע ואת הסיבה, למשל my-mod: tool.call hook skipped: threw Error: boom. איפה קוראים אותה תלוי בסשן, כפי ש-גלה למה מוד לא עושה כלום מפרט. הוק ui.render שה-עץ שלו לא עובר אימות נחשב כהוק שהחזיר צורה לא נכונה.

כדי שהוק שחוסם קריאות ייכשל במצב סגור, הוסף מטפל שגיאות .catch שעונה במקומו. כאן guard הוא פונקציית ההוק שלך, והמטפל בודק את next.called כדי לדעת אם guard כבר קרא ל-next כשהוא נכשל:

// on returns a registration, and .catch attaches a handler to that one hook
on('tool.call', { tool: 'Bash' }, guard).catch(async ($, e, next) => {
  // guard had already called next, so return what came back
  if (next.called) return next(e)
  // next.error.kind says why the handler was asked, such as 'throw' or 'timeout'
  return { deny: 'The command guard failed, so this command was not run: ' + next.error.kind }
})

כש-guard זורק או מגיע ל-timeout בקריאת Bash, Claude Code קוראת למטפל עם אותו אירוע:

  • guard נכשל לפני שקרא ל-next: הפקודה לא רצה, ו-Claude קורא את טקסט ה-deny עם ה-kind בסוף
  • guard נכשל אחרי שקרא ל-next: ה-next(e) של המטפל מושלם לתוצאה שהקריאה של guard הפיקה, בלי להריץ את הפקודה שוב, ו-Claude קורא את התוצאה הזו

למטפל יש מגבלת זמן קצרה יותר משלו. אם המטפל עצמו זורק או מגיע ל-timeout, Claude Code מדלגת על ההוק כאילו לא היה לו מטפל. כש-guard לא קרא ל-next, הפקודה ממשיכה כמו שהייתה ממשיכה בלי המוד.

אותה צורת מטפל מתאימה לשומר על prompt.submit או config.set. כש-next.called הוא false, החזר את הסירוב ש-הפניה לאירועים מציינת עבור האירוע הזה: { drop: 'the reason' } עבור prompt.submit, { deny: 'the reason' } עבור config.set.

ב-tool.check וב-plugin.register, סירוב שמוחזר אחרי ש-next הושלם עדיין נשאר בתוקף, אז החזר אותו בלי לבדוק את next.called:

ב-tool.call, deny שמוחזר אחרי ש-next הושלם מונע את התוצאה מ-Claude.

#הצעדים הבאים