מדריך גרוק CLI בעברית

תיעוד 34

הוקים

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


#מהם הוקים?

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

  • חסימת פעולות: הוק PreToolUse יכול לסרב לפקודה מסוכנת לפני שהיא רצה.
  • השארת הסוכן בעבודה: הוק Stop יכול לחסום את הסוכן מלסיים את התור שלו עד שמתקיים תנאי (למשל חבילת הבדיקות עוברת) ולהחזיר את הסיבה למודל.
  • תגובה לאירועים: הוק PostToolUse יכול לרשום ביומן כל הרצת כלי לקובץ.
  • תיקון קריאה אחרי שהיא רצה: הוק PostToolUse יכול לומר למודל מה משמעות תוצאת כלי, או להחליף את הפלט שהמודל קורא, להסתיר סוד, לקצר קיר של שורות יומן, בזמן שהתוצאה האמיתית נשארת בתיעוד.
  • הגדרת הקשר: הוק SessionStart יכול לייצא משתני סביבה או להריץ סקריפטי הגדרה.

#מקרי שימוש נפוצים

  • מנגנוני בטיחות: חסימת פקודות כמו rm -rf / לפני שהן רצות.
  • יומן ביקורת: רישום שימוש בכלים וסשנים לקובץ או לשירות חיצוני.
  • התראות: שליחת הודעה כשמשימה מסתיימת.
  • עיצוב אוטומטי: הרצת cargo fmt או prettier אחרי עריכות.
  • הגדרת סביבה: ייצוא משתנים בתחילת סשן.
  • זרימות עבודה מותאמות: הפעלת בניות, בדיקות או פריסות באירועים מסוימים.

#התחלה מהירה

  1. צור את תיקיית ההוקים:

    mkdir -p ~/.grok/hooks
  2. צור קובץ הוק, למשל ~/.grok/hooks/session-start.json:

    {
      "hooks": {
        "SessionStart": [
          {
            "hooks": [
              { "type": "command", "command": "echo \"Grok session started in workspace $GROK_WORKSPACE_ROOT\"" }
            ]
          }
        ]
      }
    }
  3. התחל (או הפעל מחדש) סשן של גרוק. ההוק רץ אוטומטית ב-SessionStart.

    ההוק הזה רץ בשורש סביבת העבודה, שורש מאגר ה-git או ה-worktree שבו אתה מפעיל את גרוק. מחוץ למאגר, או במקום שבו גרוק קורא רק את תיקיית ההפעלה (ראה מיקומי הוקים), שורש סביבת העבודה הוא תיקיית ההפעלה. גם הוקים של חבילה עצמה רצים שם. כשמפעילים מתת-תיקייה, $(pwd) בהוק הזה מדפיס את שורש סביבת העבודה, לא את תת-התיקייה. הוק רץ בתיקייה הזו כשקישורי symlink מפוענחים בזמן שההוקים נטענו. קישור שמשתנה אחר כך לא יכול להזיז אותו. התיקייה שבה הסשן התחיל היא שדה cwd של האירוע ב-stdin (ראה קלט), כפי שנכתב כשהפעלת את גרוק.

  4. הרץ /hooks ובדוק בלשונית Hooks כדי לאשר שהוא נטען.


#מיקומי הוקים

הוקים מתגלים מכמה מקומות (כולם ממוזגים):

היקףנתיבמהימן?הערות
גלובלי~/.grok/hooks/*.jsonאחרי אמון בתיקייההוקים אישיים
גלובלי~/.claude/settings.json (וגם settings.local.json)אחרי אמון בתיקייהתאימות ל-Claude Code (ניתן להגדרה)
גלובלי~/.cursor/hooks.jsonאחרי אמון בתיקייהתאימות ל-Cursor (ניתן להגדרה)
פרויקט<project>/.grok/hooks/*.jsonדורש אמוןאוטומציה לכל מאגר
פרויקט<project>/.claude/settings.json (וגם settings.local.json)דורש אמוןתאימות ל-Claude (ניתן להגדרה)
פרויקט<project>/.cursor/hooks.jsonדורש אמוןתאימות ל-Cursor (ניתן להגדרה)
תצורה~/.grok/config.tomlאחרי אמון בתיקייהההוקים שלך לצד שאר התצורה שלך
תצורהmanaged_config.toml ($GROK_HOME ו-/etc/grok)תמידהוקים שהארגון מפיץ (מסונכרנים מהשרת ועל המכשיר)
תצורהrequirements.toml (מטמון חתום, ו-/etc/grok)תמידהוקים שהארגון אוכף. ראה הוקים נאכפים
תצורה~/.grok/requirements.toml בלי חתימה תואמתאחרי אמון בתיקייהנטען כהוקים שלך
תוסףארוזים בתוך תוספים מותקניםלפי תוסף, אחרי אמון בתיקייההוקים משותפים לצוות

<project> הוא שורש המאגר וכל תיקייה מתחתיו עד התיקייה שבה גרוק הופעל. קבצי הוק של חבילה במונורפו נטענים לצד אלה של השורש. הוקים של חבילה נקראים project/<package path>/… ורצים בשורש סביבת העבודה כמו אלה של השורש, כש-CLAUDE_PROJECT_DIR מוגדר לשורש סביבת העבודה. הוק של חבילה שנכתב כ-"$CLAUDE_PROJECT_DIR"/.claude/hooks/deny.sh מריץ את הסקריפט של השורש. כשלשורש אין סקריפט כזה, ההוק לא יכול להתחיל ונכשל פתוח, כמו כל הוק שלא יכול להתחיל. גרוק מדווח על שגיאת הוק והקריאה ממשיכה, גם עבור הוק של סירוב. כדי להריץ את הסקריפט של החבילה עצמה, כתוב "$CLAUDE_PROJECT_DIR"/packages/app/.claude/hooks/deny.sh. מטפל שגם הקבצים של השורש וגם שם של חבילה מציינים רץ פעם אחת. קובץ הוק שמתפרש לאחד שמקור מוקדם יותר כבר טען, כמו .claude של חבילה שמקושר לשורש או קובץ פרויקט שמקושר ל-~/.claude/settings.json, נטען פעם אחת, מהמקור המוקדם יותר. מחוץ למאגר, נקראת רק תיקיית ההפעלה. כך גם מאגר ששורשו הוא שורש מערכת קבצים או כונן. כך גם מאגר ששורשו בתיקייה זמנית משותפת כמו /tmp או מעליה. להפעלה בתוך תיקיית הבית שלך, כך גם מאגר ששורשו בתיקיית הבית שלך או באחד מההורים שלה. מאגר ש-git לא יכול לפתוח, כמו אחד שבדיקת safe.directory מסרבת לו, לא טוען הוקי פרויקט. גרוק מדווח על השגיאה. בהפעלה בתיקיית הבית שלך, בשורש מערכת קבצים או כונן, או בתיקייה זמנית משותפת כמו /tmp, גרוק לא טוען הוקי פרויקט.

הוקים של קובץ תצורה נמצאים באותו TOML שהארגון שלך כבר שולט בו. ראה הוקים בקובצי תצורה לפורמט. מקורות הוקים של ספקים תואמים נסרקים כברירת מחדל. כדי לכבות סריקה לספק מסוים, הגדר [compat.<vendor>] hooks = false ב-~/.grok/config.toml או במשתנה הסביבה המתאים. ראה תצורה לפרטים.

מתן אמון בפרויקט: בפעם הראשונה שאתה פותח פרויקט עם הוקים, עליך לתת לו אמון לפני שהוקי הפרויקט שלו ירוצו. עד אז הם מדולגים בשקט. תן אמון על ידי הרצת /hooks-trust (או הפעלה עם --trust). ההחלטה נרשמת במאגר האמון המאוחד בתיקיות (~/.grok/trusted_folders.toml), אותו שער ששולט בשרתי MCP/LSP מקומיים למאגר. כמו ב-Claude Code, תיקייה שאינה מהימנה גם מעכבת את ההוקים שלך: אלה ב-~/.grok/hooks/, hooks-paths, ~/.grok/config.toml, ~/.grok/requirements.toml לא חתום, ~/.claude/settings.json ו-~/.cursor/hooks.json, ה-hooks: ב-frontmatter של הסוכנים שלך, והוקים של התוספים שלך. כל עוד יש לך הוק כזה, אמון בתיקיות שואל בכל תיקייה מלבד תיקיית הבית שלך ושורשי מערכת קבצים או כונן, שנחשבים מהימנים. רק הוקי managed_config.toml ו-requirements.toml של הארגון שלך רצים לפני שאתה נותן אמון בתיקייה. הרצה בלי ממשק, כמו grok -p, מחשיבה את התיקייה כמהימנה עבור ההוקים שלך, אבל הוקי פרויקט והוקים של סוכן פרויקט עצמו עדיין דורשים שהתיקייה תהיה מהימנה. בהרצה הזו, פקודה בהגדרות ~/.claude שלך או ב-hooks: של סוכן שהיא רק נתיב יחסי, כמו .claude/hooks/check.sh, נכשלת עם הודעה שהתיקייה אינה מהימנה, בעוד פקודה עם ארגומנטים או עם תחביר מעטפת עדיין רצה. זה מונע ממאגרים לא מהימנים להריץ קוד שרירותי.

מכיוון שהוקים מאוחדים תחת אמון בתיקיות, הענקת --trust / /hooks-trust נותנת אמון בתיקייה כולה עבור MCP, LSP, הוקים, הוראות פרויקט ומיומנויות פרויקט יחד, ומכסה תת-תיקיות של אותו מאגר. checkout מקונן של git מתחת לתיקייה הזו הוא סביבת עבודה נפרדת ואינו מכוסה. ולהפך, כיבוי אמון בתיקיות (GROK_FOLDER_TRUST=0 או [folder_trust] enabled = false) פותח את השער של המשטחים האלה יחד.


#אירועי הוק

אירועים נורים בשלושה קצבים: פעם לסשן (SessionStart, SessionEnd), פעם לתור (UserPromptSubmit, Stop, StopFailure), ובכל קריאת כלי בתוך התור (PreToolUse, PostToolUse, PostToolUseFailure).

אירועמתי הוא נורהחוסם?
SessionStartסשן מתחיל, או גרוק פותח מחדש סשן שמור. לא נורה לסשן של תת-סוכן עצמו. דחיסה יורה רק את PreCompact ו-PostCompact, אף פעם לא את SessionStart.לא
UserPromptSubmitאתה שולח פרומפט.כן: יכול לחסום את הפרומפט
PreToolUseכלי עומד לרוץ.כן: יכול לסרב
PostToolUseכלי מסיים לרוץ (כולל שגיאה לוגית מובנית כמו יציאה שאינה אפס של run_terminal_command. כשל בשיגור או תוצאת שגיאה של MCP יורים את PostToolUseFailure במקום).לא, אבל הוא יכול להזין למודל משוב ולהחליף את הפלט שהמודל רואה
PostToolUseFailureכלי נכשל בשיגור, או כלי MCP מחזיר תוצאת שגיאה.לא, אבל הוא יכול להזין למודל additionalContext ובמסלול המעטפת להחליף את טקסט השגיאה שהמודל רואה
PermissionDeniedמערכת ההרשאות מסרבת לקריאת כלי.לא
Stopתור של סוכן מסתיים בהשלמה אמיתית (הפרעה יורה את StopCancelled במקום).כן: יכול לחסום את העצירה
StopFailureתור מסתיים בגלל שגיאת API. systemMessage מההוקים שלו לא מוצג.לא
StopCancelledרץ במקום Stop כשתור מסתיים בלי להשלים: הפרעת משתמש (Ctrl+C / עצירה מהלקוח), הנחיית הרשאה שנדחתה, מגבלת --max-turns, יציאה בגלל חוסר התקדמות, או continue: false של הוק כלי.לא
Notificationאירועי תשומת לב של משתמש (idle_prompt, permission_prompt, task_complete, …).לא
SubagentStartתת-סוכן מתחיל.לא
SubagentStopתור של תת-סוכן מסתיים (נורה פעם אחת, בתת-הסוכן, עם בקרת החלטת עצירה).כן: יכול לחסום את העצירה
PreCompactדחיסת שיחה עומדת לרוץ. systemMessage מההוקים שלו לא מוצג.לא
PostCompactדחיסת שיחה מסתיימת. systemMessage מההוקים שלו לא מוצג.לא
SessionEndהסשן מסתיים. נושא subagentType לסשן ילד, כדי שמארח יוכל להבדיל בין פירוק של ילד לפירוק שלו. systemMessage מההוקים שלו לא מוצג.לא

SubagentEnd מתקבל ככינוי ל-SubagentStop. PreToolUse יכול לחסום קריאת כלי, UserPromptSubmit יכול לחסום פרומפט (ראה למטה), ו-Stop/SubagentStop יכולים לחסום את הסוכן מלעצור (ראה בקרת החלטת עצירה). PostToolUse רץ מאוחר מדי כדי לחסום משהו, אבל ה-stdout שלו נקרא: הוא יכול להזין למודל משוב ולהחליף את פלט הכלי שהמודל רואה (ראה פלט PostToolUse). כל אירוע אחר הוא סביל.

#בקרת החלטה של UserPromptSubmit

הוק UserPromptSubmit יכול לדחות פרומפט: יציאה 2 חוסמת (stderr הופך להודעה), וכך גם JSON {"decision": "block", "reason": "..."} ב-stdout, בכל קוד יציאה. {"continue": false, "stopReason": "..."} חוסם גם הוא, בלי קשר למה שההחלטה אומרת. ההודעה שלו היא stopReason, אחרת reason של החסימה, אחרת stderr הגזום של ההוק, אחרת "Prompt blocked by hook ''". הסיבה מוצגת לך ולעולם לא מתווספת להקשר של המודל. רק פרומפט שהקלדת יכול להיחסם: תורות של התעוררות אוטומטית (השלמות של משימה ושל תת-סוכן, הפעלות של המתזמן) וסשנים של תת-סוכן מריצים את ההוק לצפייה בלבד. פסק הזמן ברירת המחדל לאירוע הזה הוא 30 שניות. הוק שתם הזמן שלו או שקרס נכשל פתוח והפרומפט ממשיך. מטפל עם "failClosed": true חוסם במקום זאת את הפרומפט, כשהכשל הוא ההודעה (ראה שדות מפתח). תשובה שעברה מגבלת קריאה חוסמת בכל קוד יציאה מלבד 2 (ראה קריאת תשובת הוק).

אחרי חסימה, פרומפטים שכבר בתור מאחורי החסום לא רצים אוטומטית: התור מחכה עד שאתה פועל (שליחת פרומפט, או עריכה / הסרה / סידור מחדש / הרצה מאולצת של שורה בתור). פרומפט חסום לא נרשם: הוא אף פעם לא נכנס להיסטוריית השיחה שהמודל רואה בתורות מאוחרים, לרשומת הסשן על הדיסק, או לסיכום הסשן. הוא נשאר גלוי בגלילה החיה ומוחזק בחזית התור כדי שתערוך, תשלח מחדש או תזרוק. אחרי הפעלה מחדש של הסשן בועת החסימה נעלמת מהגלילה, בדיוק כי שום דבר לא נשמר. חריג מכוון אחד: היסטוריית הפרומפטים המקומית של הלקוח שלך (שליפה בחץ למעלה) שומרת את הטקסט, שנרשם בזמן השליחה לפני שההוק רץ, כך שפרומפט שנזרק עדיין ניתן לשחזור. מגבלה נוכחית אחת: stdout של הוק מתיר נזרק (אין additionalContext).

#תאימות להוקים של Cursor

גרוק מריץ את קבוצות ה-matcher ב-~/.cursor/hooks.json וב-.cursor/hooks.json. קבוצת matcher היא רשומה עם רשימת hooks, הצורה שקובץ הגדרות של Claude מחזיק. הרשומות של Cursor עצמו, שנכתבות ישירות מתחת לאירוע כ-{"command": "...", "matcher": "...", "timeout": 30}, עדיין לא רצות. הוק של Cursor עונה בפורמט התשובה של Cursor (permission, continue). גרוק עדיין לא קורא את הפורמט הזה. סירוב שהוק כזה הדפיס לא יחסום. כל רשומה כזו מתחת לאירוע שגרוק מכיר מדווחת כשגיאת הטעינה "this Cursor hook won't run until Grok reads Cursor hook replies". שאר הקובץ עדיין נטען. רשומה מתחת לאירוע שגרוק לא מכיר, כמו afterFileEdit של Cursor, מדולגת עם האירוע שלה ורק נרשמת ביומן. כל מקור הוקים אחר (הגדרות Claude, hooks.json של תוסף, קובצי הוק של גרוק ו-config.toml) קורא רק קבוצות matcher. רשומה שם בלי רשימת hooks היא שגיאת טעינה ולעולם לא רצה.

גרוק מקבל את שמות אירועי ההוק של Cursor ב-camelCase:

אירוע Cursorממופה אל
sessionStart, sessionEndSessionStart, SessionEnd
preToolUse, postToolUse, postToolUseFailurePreToolUse, PostToolUse, PostToolUseFailure
beforeShellExecution, beforeMCPExecution, beforeReadFilePreToolUse
afterShellExecution, afterMCPExecution, afterFileEditPostToolUse
afterAgentResponse, afterAgentThoughtPostToolUse
beforeSubmitPromptUserPromptSubmit
subagentStart, subagentStopSubagentStart, SubagentStop
preCompact, stopPreCompact, Stop

הוקים לפי פעולה של Cursor (beforeShellExecution, afterFileEdit וכו') ממופים לאירועים הגנריים PreToolUse/PostToolUse. סקריפט ההוק מקבל את שם הכלי בקלט ה-JSON ויכול לסנן בהתאם, או להשתמש בשדה matcher.

#פורמט ה-JSON של ה-Hook

כל קובץ .json יכול להגדיר hooks למספר אירועים:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "bin/safety-check.sh", "timeout": 10 }
        ]
      }
    ],
    "PostToolUse": [
      {
        "hooks": [
          { "type": "command", "command": "bin/log-activity.sh" }
        ]
      }
    ]
  }
}

#שדות מרכזיים

  • שם אירוע (מפתח ברמה העליונה): כל אירוע שמופיע ב-אירועי Hook. Grok מדלג על שמות אירועים שאינם מזוהים, כך שקובץ הגדרות משותף של Claude או Cursor עדיין נטען.
  • matcher (אופציונלי): שם, רשימת שמות, או ביטוי רגולרי. הוא בוחר את ההפעלות שמפעילות את ה-hook (ראו תחביר Matcher). מה שהוא בודק תלוי באירוע: שם הכלי באירועי כלים (PreToolUse, PostToolUse, PostToolUseFailure, PermissionDenied), סוג ההתראה ב-Notification, סוג ה-subagent ב-SubagentStart/SubagentStop (למשל explore), מקור ההתחלה ב-SessionStart (new לסשן חדש, כולל סשן מ-/new או /clear, או load לסשן שמור שנפתח מחדש עם --resume, --continue, או בורר /resume. הערכים startup, resume, clear ו-compact של Claude Code אף פעם לא תואמים), סיבת הסיום ב-SessionEnd, טריגר הדחיסה ב-PreCompact/PostCompact (manual או auto), סוג השגיאה ב-StopFailure (rate_limit, authentication_failed, invalid_request, server_error, max_output_tokens, או unknown), והסיבה ב-StopCancelled (user_interrupt, permission_rejected, permission_cancelled, max_turns, no_progress, hook_stopped, או unknown). matcher על Stop או UserPromptSubmit מתעלמים ממנו עם אזהרה (האירועים האלה תמיד נורים). matcher ריק או שהושמט תואם הכל. צלצול של סיום חשיבה צריך להגדיר את matcher ל-idle_prompt על Notification (סוף כל תור, ואז חוסר פעילות מתמשך). permission_prompt נורה רק כשממשק הרשאות באמת ממתין. ה-matcher בודק את שם הכלי האמיתי. קריאות MCP שמנותבות דרך ה-dispatchers הפנימיים use_tool / CallMcpTool של Cursor מופיעות כשם המלא server__tool (למשל linear__save_issue). התאימו לשם הזה, לא לשם ה-dispatcher. matcher יכול גם להשתמש בשם mcp__server__tool של Claude Code.
  • type: "command" (הרצת סקריפט או שורת מעטפת) או "http" (POST של האירוע לכתובת URL).
  • command: נתיב לקובץ הרצה (יחסי לקובץ ה-JSON) או פקודת מעטפת בתוך השורה. שם חשוף כמו my-guard מריץ את הקובץ בשם הזה ליד קובץ ה-JSON, או את my-guard שב-PATH כשאין קובץ כזה. שם חשוף מתעלם מרשומות PATH יחסיות כמו . או node_modules/.bin. בקובץ הגדרות של Claude (.claude/settings.json או .claude/settings.local.json), נתיב יחסי עם /, כמו .claude/hooks/check.sh, נלקח משורש סביבת העבודה, התיקייה שממנה Claude מריץ אותו. מרכאות סביב פקודה שלמה שהיא נתיב אחד בלבד מקבצות אותו, כולל רווחים וגרשיים. "/opt/my hooks/guard.sh" מריץ את /opt/my hooks/guard.sh, ו-Grok מפעיל אותו ישירות, כמו שהוא עושה לנתיב בלי מרכאות, ולכן ב-Linux וב-macOS סקריפט צריך שורת #!, במקום שבו sh -c פעם הריץ סקריפט בלי שורה כזאת. ".claude/O'Brien hooks/check.sh" בקובץ הגדרות של Claude הוא נתיב יחסי כמו כל נתיב אחר. מרכאות בודדות שומרות על כל מה שבתוכן כפי שהוא, ולכן '/opt/R&D/$5/guard.sh' הוא גם נתיב אחד. נתיב במרכאות כפולות שמכיל $ או backtick עדיין רץ במעטפת, שמרחיבה אותו. תחת sh ו-Git Bash, כך גם נתיב עם \ לפני \ אחר או לפני מעבר שורה, או בסופו, בעוד "bin\check.sh" נותן לקובץ את השם bin\check.sh. תחת cmd, כך גם נתיב שמכיל %, כמו "%ProgramFiles%\My Tool\guard.exe". sh ו-cmd קוראים ~ בתוך מרכאות כתיקייה בשם הזה, ולכן "~/guard.sh" הוא נתיב יחסי כמו כל נתיב אחר. PowerShell קורא ~ מוביל כתיקיית הבית, ולכן שם מילה במירכאות שמתחילה ב-~ רצה במעטפת, וכך גם מילה שאינה נתיב תוכנית, כמו הודעה או תשובת JSON. תחת PowerShell, המילה המצוטטת הפותחת של פקודה רצה כתוכנית במקרים ש-פקודות מעטפת תחת PowerShell מפרטת. ב-Windows, נתיב או שם חשוף של .ps1 רץ עם -File תחת PowerShell שזוהה. תחת Git Bash או cmd, זה pwsh כשהוא ב-PATH, אחרת Windows PowerShell. Windows מפעיל בעצמו נתיב או שם חשוף של .exe, .cmd, .bat או .com. תחת Git Bash, כל דבר אחר, כמו סקריפט .sh או .py או קובץ בלי סיומת, רץ דרך Git Bash, שקורא את שורת #! שלו. תחת PowerShell או cmd, קובץ עם סיומת אחרת נכשל עם הודעה, כי הם היו פותחים אותו באפליקציה של סוג הקובץ ומעבירים לו אף אחד מקלט ה-hook. זה נכון לכל מה ש-PATHEXT מפרט: תחת PowerShell, התחילו את הפקודה ב-&, כמו & 'C:\hooks\guard.py', כדי לקרוא לקובץ שהסיומת שלו נוספת. Grok מפעיל את PowerShell של כל hook עם -ExecutionPolicy Bypass ומגדיר PSExecutionPolicyPreference=Bypass לכל hook, כך ש-PowerShell שה-hook מפעיל מריץ גם סקריפטים, תחת כל מעטפת. מדיניות ביצוע של Group Policy עדיין חלה.
  • args (אופציונלי): מערך args קיים, כולל [], הופך את command לתוכנית אחת. הרכיבים הם הארגומנטים שלה. המריץ לא מפעיל מעטפת. פקודה שנשאה ארגומנטים משלה שמה אותם ב-args. command נשאר שם התוכנית או הנתיב. כל רכיב הוא ארגומנט אחד. רווחים, מרכאות, $ ו-backticks נשארים כפי שנכתבו. Grok ממלא ארגומנט מ-env של ה-handler, ממשתני התוסף כשהתוסף הגדיר אותם (${CLAUDE_PLUGIN_DATA} רק כשלתוסף יש תיקיית נתונים), וממשתני המריץ (GROK_HOOK_EVENT, GROK_HOOK_NAME, GROK_SESSION_ID, GROK_WORKSPACE_ROOT, CLAUDE_PROJECT_DIR), גם בצורה $NAME וגם בצורה ${NAME}. command שמתחיל ב-${CLAUDE_PROJECT_DIR} או $CLAUDE_PROJECT_DIR ומפריד נתיב הוא תוכנית תחת שורש סביבת העבודה כשהשאר הוא נתיב יחסי לא ריק. כל שם אחר נשאר כפי שנכתב. זה כולל $HOME, שם שלא הוגדר, ${NAME:-default}, ~, מרכאות ו-globs. handler מסוג http מתעלם מ-args.
  • timeout: שניות לפני הריגת ה-hook, כל מספר לא שלילי כמו 1.5 (ברירת מחדל: 5, או 600 ל-Stop/SubagentStop/PostToolUse). timeout של 0 שומר על ברירת המחדל. כל כשלי ה-hook (חריגות זמן, קריסות, פלט לא תקין) הם fail-open: הכשל נרשם ל-scrollback של הממשק אבל קריאת הכלי לא נחסמת. רק החלטת deny מפורשת, תשובה מעבר למגבלת קריאה, או updatedInput שאי אפשר להחיל חוסמים קריאת כלי. אי אפשר להחיל updatedInput כשהוא מקונן מעבר ל-124 רמות או נכשל בסכמה של הכלי (ראו קריאת תשובת Hook).
  • failClosed (אופציונלי, ברירת מחדל false. גם fail_closed נקרא): handler עם "failClosed": true חוסם כשההרצה שלו נכשלת. חריגת זמן, קריסה, כשל בהפעלה, או תשובה שלא היה אפשר לקרוא (גוף שמתחיל ב-{ ולא מתפרס, או ערך החלטה לא מוכר) אז דוחים את קריאת הכלי (PreToolUse) או חוסמים את ההנחיה (UserPromptSubmit), עם סיבה שמציינת את ה-hook ואת הכשל. ה-handlers שאחריו לא רצים. תשובה שפורסרה עדיין מחליטה כרגיל. stdout שאינו JSON ביציאה 0 הוא עדיין allow. failClosed משנה רק את מה שהרצה שנכשלה עושה. רק PreToolUse ו-UserPromptSubmit תומכים בו. בכל אירוע אחר השדה מתעלמים ממנו עם אזהרה בלוג. Claude Code כותב את אותה בחירה "onFailure": "block" (ברירת המחדל שלו היא "continue"). Grok קורא גם את הכתיב הזה, בקבצי ה-hook שלו ובקובץ הגדרות משותף של Claude. Cursor משתמש באותו שם failClosed כמו Grok. ערך onFailure לא מוכר נקרא כ-"continue" עם אזהרה בלוג.

#פקודות מעטפת תחת PowerShell

ב-Windows תחת pwsh או Windows PowerShell, sh -c היה מריץ את המילה המצוטטת הפותחת של פקודה כתוכנית, אבל PowerShell קורא אותה כמחרוזת. המילה הפותחת היא הראשונה אחרי שורות ריקות, הערות והמשכי שורה ב-backtick, ואחרי & מוביל וכל הערה או המשך בשורה שלו. Grok שם את אופרטור הקריאה & לפניה במקרים האלה.

המילה המצוטטת הפותחת רצה כתוכנית כאשר:

  • ארגומנטים באים אחריה: "C:\Program Files\nodejs\node.exe" guard.js.
  • היא לבד, או לפני ;, מעבר שורה, הערת #, &&, || או &, והיא נתיב תוכנית. נתיב תוכנית לא מכיל ://, ומסתיים ב-.exe, .cmd, .bat, .com או .ps1 או מכיל \ או /. עם רווח, הוא חייב להכיל \ או /, וגם להסתיים באחת מהסיומות האלה, להתחיל בשורש (C:\, \\server\, .\, ..\, ~\, $env:ProgramFiles\, אותו דבר עם /, או \ או / עם אחד נוסף אחריו), או להסתיים בסיומת אחרת עם אות, כמו .sh, כשה-\ או / הראשון שלו לפני הרווח הראשון. לכן 'Blocked: see docs/deploy.md' היא הודעה, וכך גם "my hooks/check.sh": כתבו אותו כ-"./my hooks/check.sh".
  • שאר הנתיב בא ישירות אחרי התיקייה המצוטטת: "$CLAUDE_PROJECT_DIR"/.claude/hooks/check.cmd רץ כתוכנית אחת.
  • הפקודה מתחילה ב-$CLAUDE_PROJECT_DIR או במשתנה hook אחר.

היא נשארת מחרוזת, כפי ש-PowerShell קורא אותה, כאשר:

  • היא אינה נתיב תוכנית, כמו 'Use pnpm, not npm' או 'https://wiki/deploy-policy', או שהיא מכילה " או $(…), כמו תשובת JSON. PowerShell מדפיס אותה.
  • צינור, הפניה כמו 2>&1, או טקסט שצמוד אליה כמו .Length או [0] באים אחריה.
  • אופרטור PowerShell והערך שלו באים אחריה, כמו - 1 או -eq 'x', או -f וערך אחרי מחרוזת שמכילה {.

כדי להריץ מילה כזאת בכל זאת, התחילו את הפקודה ב-& "…".

היא לא רצה, והקריאה שלה נכשלת עם הודעה, כאשר:

  • זה קובץ ש-Windows לא יכול להפעיל כתוכנית, כמו סקריפט .sh, .py או .js, גם כשה-& שלכם קורא לו עם נתיב מצוטט או נתיב שמתחיל במשתנה hook. PowerShell היה פותח אותו באפליקציה של סוג הקובץ ומעביר לו אף אחד מקלט ה-hook. נתיב בלי מרכאות, כמו & .\check.sh או C:\hooks\check.sh --strict, לא נתפס ונפתח כך.

בפקודת מעטפת, קובץ שהסיומת שלו נוספת על ידי PATHEXT, כמו .py, נקרא כפי ש-PowerShell קורא לו. זה PATHEXT של env של ה-hook כשהוא מגדיר אחד. פקודה שהיא רק הנתיב המילולי של קובץ כזה, עם מרכאות או בלי, או השם החשוף שלו, אינה טקסט מעטפת, ולכן היא עדיין נכשלת (ראו command למעלה), עם הודעה שמציינת את & '<full path>' לכתוב במקום. נתיב שמתחיל במשתנה hook או, תחת PowerShell, ~ מצוטט, כמו "$CLAUDE_PROJECT_DIR"/hooks/guard.py או "~\hooks\guard.py", הוא טקסט מעטפת, ולכן הוא נקרא. הרשומות של Windows Script Host ושל מסוף הניהול של Windows עצמו, כמו .js, .vbs ו-.msc, לא נקראות כך, כי הן נפתחות בחלון משלהן.

המשפטים אחרי קריאה שלא רצה עדיין רצים, ולכן ; exit 2 בסוף או & exit 2 עדיין חוסמים. התחילו את הפקודה בתוכנית שמריצה את הקובץ, כמו python או "C:\Program Files\Git\bin\bash.exe" של Git Bash. bash חשוף תחת PowerShell הוא של WSL או לא קיים, כי Git for Windows שם רק את Git\cmd ב-PATH. בהגדרות שמשותפות לפלטפורמות אחרות או ל-Claude Code, הגדירו GROK_SHELL=bash במקום, כדי שהפקודה תרוץ תחת Git Bash.

#תחביר Matcher

התווים ב-matcher קובעים איך Grok קורא אותו. Claude Code משתמש באותם כללים.

Matcherנקרא כדוגמה
"*", "", או שהושמטהתאמה להכלנורה בכל מופע של האירוע
רק אותיות, ספרות, _, -, רווחים, ,, ו-|שם אחד, או רשימת שמות מופרדת ב-| או ,. רווחים סביב שם מתעלמים מהם. כל שם מושווה בדיוק.read_file תואם רק את הכלי הזה. Edit|Write ו-Edit, Write כל אחד תואם אחד משני הכלים. code-reviewer תואם רק את סוג הסוכן הזה.
כל דבר אחרביטוי רגולרי שיכול להתאים לכל חלק מהשם, או לכל שם כלי של Claude או של הלקוח שממופה לכלי בטבלת הכינויים (ראו כינויי שמות כלים)^Notebook תואם כל כלי שהשם שלו מתחיל ב-Notebook, וגם את search_replace, hashline_edit, StrReplace ו-EditNotebook, כי NotebookEdit ממופה אליהם. Edit.* תואם את Edit ואת NotebookEdit.

matcher של StopFailure מפרט סוגי שגיאה רק עם |. , , רווח, או - בתוכו הופכים אותו לביטוי רגולרי. rate_limit|server_error תואם אחד משני הסוגים, ו-rate_limit,server_error ו-rate_limit, server_error לא תואמים אף אחד. Grok רושם אזהרה כשהוא טוען matcher של StopFailure שנכתב כרשימת שמות עם ,, רווח, או -.

עגנו ביטוי רגולרי עם ^ ו-$ (כמו ב-^Edit$) כשצריך התאמה לשם שלם.

Grok נותן לכלי MCP את השם <server>__<tool>, למשל github__search_repositories. Claude Code נותן לו את השם mcp__<server>__<tool>. Grok גם בודק matcher שמכיל mcp__ מול השם של Claude Code. mcp__github__.* מיובא ו-github__.* מקומי שניהם בוחרים כל כלי משרת github. mcp__github__search_repositories תואם רק את הכלי הזה. mcp__github חשוף לא תואם שום כלי, ב-Grok או ב-Claude Code. הוסיפו __.* כדי להתאים כל כלי בשרת. matcher בלי mcp__ אף פעם לא נבדק מול השם של Claude Code.

#שדות Handler שאינם נתמכים

Grok קורא מ-handler רק את type, command, args, url, timeout, failClosed, onFailure ו-env. כש-handler שכן ירוץ מגדיר אחד משדות Claude Code שלמטה, Grok רושם שגיאת טעינת hook שמציינת את השדה ואת קובץ ה-hook. handler שלא יכול לרוץ מדווח על הסוג שלו או על שדה חסר. השגיאה נכתבת ללוג המעקב (hook loading error בהפעלה, hook reload error בטעינה מחדש, ו-hook loading או hook loading from settings file בזמן שקובץ hook נקרא) ומוחזרת כ-load_errors בפרוטוקול ה-hooks. לשונית Hooks ב-pager מציגה כל שגיאת טעינה כשורת Error: מעל קבוצות ה-hooks. לחיצה על Enter בשורה מציגה את כל ההודעה. grok inspect מציג אותן תחת Hook Load Errors. ל-handler שדולג אין שורת hook משלו. שאר ה-handlers בקובץ עדיין נטענים.

שדהמה Grok עושה
ifמדלג על ה-handler. hook נאכף עדיין רץ. בלי if, ה-handler היה רץ בכל קריאה שה-matcher שלו בוחר. שגיאה של handler שדולג מציינת רק את השדה הזה, לא את השדות שלמטה.
async, asyncRewake, shell, statusMessage, headersטוען את ה-handler ומריץ אותו בלי השדה. ה-handler רץ בחזית. פקודה בלי args רצה כ-sh -c ב-Unix ותחת מעטפת Windows שזוהתה (pwsh, Windows PowerShell, או Git Bash) ב-Windows. אין הודעת סטטוס ואין כותרות HTTP נוספות.

Grok לא מדווח על שדה שהערך שלו לא מבקש כלום. הערכים האלה הם null, false, מחרוזת ריקה, רשימה ריקה, ואובייקט ריק. לדוגמה, handler עם "statusMessage": "" נטען כרגיל. shell לא ריק מדווח, כולל "bash". הפקודה לא רצה תחת המעטפת הזאת. Grok מתעלם מכל מפתח אחר שהוא לא מזהה.

#כינויי שמות כלים

ב-matcher, Grok ממפה שמות כלים בסגנון Claude לשמות שלו כדי ש-hooks שעברו מ-Claude יירו נכון. כינויים נפוצים כוללים:

  • Bash → run_terminal_command
  • Read → read_file
  • Edit, Write ו-MultiEdit → search_replace
  • Grep → grep
  • Glob ו-LS → list_dir
  • WebSearch → web_search
  • Task → spawn_subagent

matcher שומר גם את השם המקורי שלו, ולכן Bash תואם גם את Bash וגם את run_terminal_command.

תחת ערכת Cursor (agent_type = "cursor"), שמות Claude תואמים גם את הכלים של הערכה הזאת:

  • Bash → Shell
  • Edit, Write, MultiEdit ו-NotebookEdit → StrReplace ו-EditNotebook
  • AskUserQuestion → AskQuestion
  • TaskOutput, BashOutput, BashOutputTool ו-AgentOutputTool → AwaitShell
  • Agent → Task

הכלים האלה נותנים ל-hooks מפתחות קלט משלהם. StrReplace ו-Write מציינים את הקובץ ב-path, ו-Write מחזיק את הטקסט ב-contents. EditNotebook מציין את המחברת ב-target_notebook. hook שקורא את tool_input.file_path או את tool_input.content לא קורא כלום לכלים האלה.

שמות כלי הלקוח עובדים גם כמונחי matcher בכל מסלול:

  • Shell → run_terminal_command
  • StrReplace ו-EditNotebook → search_replace ו-hashline_edit
  • List → list_dir
  • Fetch → web_fetch

matcher של EditNotebook נורה בכל עריכת search_replace ו-hashline_edit בכל מסלול, כי עריכת מחברת מגיעה ל-hooks של ה-daemon כ-search_replace. תחת ערכת Cursor, הוא עדיין לא תואם את StrReplace. ביטוי רגולרי גם מתרחב עם השמות האלה: ^List ו-List$ תואמים את list_dir, ^Fetch תואם את web_fetch, ו-Notebook$, Replace או ^Str תואמים כל עריכת search_replace ו-hashline_edit.

במסלול ה-daemon, אירוע של כלי קבצים מציין read_file, grep, search_replace או write תחת כל אחת מערכות כלי הקבצים. matcher של hashline_* אף פעם לא נורה שם. התאימו את Read, Grep, Edit או Write כדי לבחור את הכלי בשני המסלולים.

#איך הוק מוכרע

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

  1. בחירת קבוצות תואמות. לאירוע הזה, כל קבוצת matcher שה-matcher שלה תואם את השדה של האירוע רצה. ה-matcher בודק את שם הכלי באירועי כלים, את סוג ההתראה ב-Notification, וכן הלאה (ראו שדות מרכזיים). matcher ריק או מושמט תואם הכל.
  2. הרצת המטפלים לפי הסדר. המטפלים בקבוצות שנבחרו רצים לפי סדר ההגדרה, וכל אחד מקבל את האירוע כ-JSON על stdin, עד שאחד מחזיר deny (וזה עוצר את השרשרת). מטפלים ממקורות שונים (גלובלי, פרויקט, תוסף, הגדרה) ממוזגים, ומטפל שכמה קבוצות שנבחרו מציינות רץ פעם אחת לכל קריאה, גם כשה-matchers שלהן שונים. שני מטפלים זהים כשהם מריצים את אותו דבר עם אותם ארגומנטים ועם אותו env: אותו URL, או אותה פקודה שנפתרת באותו אופן עם אותם args. פקודה יחסית כמו guard.sh נפתרת מול ספריית קובץ ההוק שלה, כך ש-guard.sh ב-~/.grok/hooks/ וב-.grok/hooks/ הם שני מטפלים. כל עותק נטען, ו-/hooks מציג כל אחד. כשעותקים נבדלים ב-timeout, העותק הראשון לפי סדר ההגדרה רץ עם ה-timeout שלו, אלא אם לעותק מאוחר יותר יש סמכות גבוהה יותר (עותק מנוהל, לדוגמה), ואז הוא רץ במקום העותק הראשון. כשעותקים נבדלים ב-failClosed, ההרצה נכשלת סגור אם עותק כלשהו הגדיר את זה, יהיה העותק שרץ אשר יהיה. מטפל של תוסף או של סוכן שייך לו, ולכן הוא אף פעם לא מתמזג עם מטפל של תוסף אחר או של סוכן אחר. כל מטפל רואה את קלט הכלי המקורי של המודל. updatedInput של PreToolUse מוחל רק אחרי שכל המטפלים מסיימים, כך שמטפל אחד לא יכול לראות שכתוב של אחר (השכתוב האחרון מנצח).
  3. החלת ההחלטה. בשער PreToolUse, ה-deny הראשון חוסם את הקריאה והסיבה שלו מוצגת למודל, updatedInput משכתב את קלט הכלי, ואחרת הקריאה ממשיכה. עבור Stop ו-SubagentStop, block משאיר את הסוכן בעבודה. עבור PostToolUse הכלי כבר רץ, כך ששום דבר לא נחסם וכל הוק רץ: סיבת block וכל additionalContext נמסרים למודל עם תוצאת הכלי, והחלפת פלט משכתבת את עותק המודל של התוצאה הזו. כל אירוע אחר הוא פסיבי: הפלט שלו נרשם אבל לא משנה את זרימת הבקרה.
  4. כשל פתוח. מטפל שחורג מהזמן, קורס, או פולט פלט פגום נרשם ב-scrollback אבל אף פעם לא חוסם את הפעולה. מטפל PreToolUse או UserPromptSubmit עם "failClosed": true יוצא מהכשל הפתוח: הרצה כושלת שלו שוללת את הקריאה או חוסמת את הפרומפט (ראו שדות מרכזיים). המטפלים שאחריו לא רצים. יש שלושה חריגים. updatedInput של PreToolUse שנכשל בסכמת הכלי לא יכול לרוץ בבטחה. גרוק חוסם את הקריאה ומדווח עליה כשגיאת קלט לא תקין. updatedInput שמקונן מעבר ל-124 רמות, מהוק שהצליח, שולל את הקריאה. תשובת PreToolUse או UserPromptSubmit מעבר למגבלת קריאה שוללת את הקריאה או חוסמת את הפרומפט (ראו קריאת תשובה של הוק). אחרת רק deny מפורש חוסם קריאת כלי.

#הוקים בקובצי הגדרה

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

קובץשכבהמי מגדיר
~/.grok/config.tomlמשתמשאתם
managed_config.toml ($GROK_HOME, /etc/grok)מנוהל / מערכתהארגון שלכם
requirements.toml ($GROK_HOME signed cache, /etc/grok)דרישותהארגון שלכם

ה-TOML זהה במבנה לאובייקט הוק ב-JSON, כך שהוק קיים מועתק ישירות:

[[hooks.PreToolUse]]
matcher = "Bash|Write|Edit"
hooks = [
  { type = "command", command = "/opt/guard/pretooluse.sh", timeout = 10 },
]

כל קבוצת matcher היא רשומת [[hooks.<Event>]] עם matcher אופציונלי ומערך hooks פנימי של מטפלים. שדות המטפל (type, command, args, url, timeout, failClosed, env) ושמות האירועים זהים בדיוק לאלה בפורמט ה-JSON.

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

[[hooks.PreToolUse]]
matcher = "Bash|Write|Edit"
[[hooks.PreToolUse.hooks]]
type = "command"
command = "/opt/guard/pretooluse.sh"
timeout = 10

עדיפה הצורה המוטמעת כדי להימנע מחזרה על הכותרת [[hooks.<Event>.hooks]] לכל מטפל.

  • מצטבר בין שכבות. ההוקים של כל שכבה רצים. שכבה בעדיפות נמוכה יותר מוסיפה הוקים אבל אף פעם לא מחליפה חסימה של שכבה אחרת. הוק שמוגדר באופן זהה ביותר משכבה אחת נטען מכל אחת, ורץ פעם אחת לכל קריאה: העותק עם הסמכות הגבוהה ביותר רץ.
  • תוויות מקור. הוקי הגדרה מופיעים ב-/hooks עם תגית לפי מקור (managed:, requirements/signed:, requirements/user:, user:, וכן הלאה) כך שאפשר לראות איזו שכבה תרמה כל אחד.
  • בלי הרחבה בזמן קריאה. ${VAR} מילולי ב-command או ב-url מגיע למריץ ההוק בלי שינוי, בהתאם לסמנטיקה של קובץ הוק JSON. המריץ מבצע את ההרחבה היחידה.

#הוקים נאכפים

הוקים משכבות ההגדרה שהארגון שולט בהן נאכפים: הם נושאים תג [policy] ב-/hooks, Space מסרב להשבית אותם, רשומה ב-~/.grok/disabled-hooks לא מדלגת עליהם, ואת המקור שלהם אי אפשר להסיר. שתי שכבות נכללות:

  • קובצי המערכת בבעלות root, /etc/grok/requirements.toml ו-/etc/grok/managed_config.toml (שמות requirements/system: ו-system_managed:).
  • קובץ ~/.grok/requirements.toml שסנכרון הפריסה כותב, כל עוד הבתים שלו תואמים למדיניות החתומה שהשרת שלח (שמות requirements/signed:). אם הקובץ נערך או שקובץ החתימה שלו חסר או לא קריא, ההוקים שלו נטענים כשלכם (שמות requirements/user:) ואפשר להשבית אותם שוב. ארגונים שצריכים שהקובץ יישאר שלם מגדירים fail_closed = true באותן דרישות. גרוק מסרב אז להתחיל על עותק שנערך או על חתימה חסרה. גרוק מסרב להתחיל על requirements.toml שהוא לא יכול לקרוא או לפרש, גם בלי fail_closed. ראו הגדרות.

הוקים ב-~/.grok/managed_config.toml וב-~/.grok/config.toml הם הפצה, לא אכיפה: אפשר להשבית אותם.

#לאפשר רק הוקים מנוהלים

ארגון יכול להגביל מכונה להוקים שהוא אוכף:

# /etc/grok/requirements.toml
allow_managed_hooks_only = true

כשזה מוגדר:

  • מה רץ. רק הוקים נאכפים: אלה מ-/etc/grok/requirements.toml ומ-/etc/grok/managed_config.toml שבבעלות root, ואלה ב-~/.grok/requirements.toml המסונכרן כל עוד הוא תואם לחתימה שלו. קובץ מסונכרן שנערך נטען כהוקים שלכם, והנעילה מדלגת עליהם.
  • מה מדולג. כל הוק אחר, בזמן השיגור: ~/.grok/hooks, כל קובץ ~/.grok/*.toml, הוקי פרויקט, הוקי תוסף, הוקי frontmatter של סוכן, קובצי התאימות של Claude ושל Cursor, ופקודות [[ui.notifications.hooks]] של ה-pager. ב-/hooks הם מציגים [disabled], והפעלתם נדחית ("Hooks outside managed policy are disabled by your organization."). grok inspect מציין את הקובץ שהגדיר את הנעילה.
  • מה עדיין רץ. הוקים שלקוח ההטמעה רושם דרך ACP (IDE או אפליקציית שולחן העבודה), כמו תחת allowManagedHooksOnly של Claude Code.
  • Windows. אין שכבה בבעלות root (אין /etc/grok), כך שרק הוקי requirements.toml המסונכרן והחתום והוקי לקוח ACP עדיין רצים.

המפתח הוא נעילת מדיניות להידוק בלבד: כל שכבת מדיניות מקורית (requirements.toml או managed_config.toml ב-$GROK_HOME או ב-/etc/grok, או MDM של macOS) או managed-settings.json של Claude יכולה להגדיר אותה, allowManagedHooksOnly ב-camelCase מתקבל בכל מקום, אף שכבה לא יכולה לשחרר אותה, וערך שאינו בוליאני מפעיל אותה. גרוק קורא אותה עם ההוקים בכל פעם שהוא טוען אותם, כך שנעילה שסנכרון הפריסה כותב אחרי ההפעלה חלה על הסשן הבא ועל Reload של /hooks. מרגע שהופעלה היא נשארת פעילה עד שגרוק מופעל מחדש, גם אם הקובץ שהגדיר אותה משתנה. grok inspect מציג אותה תחת נאכף על ידי מדיניות בתור "Hooks outside managed policy disabled" עם הקובץ שהגדיר אותה.


#כתיבת סקריפטים של הוקים

#קלט

האירוע נשלח כשורת JSON אחת, שמסתיימת בשורה חדשה, על stdin (לדוגמה, אירוע PreToolUse, שמפורמט כאן מחדש לקריאות. המטען כולל תמיד גם את toolUseId ואת toolInputTruncated):

{
  "hookEventName": "pre_tool_use",
  "hook_event_name": "PreToolUse",
  "sessionId": "abc-123",
  "cwd": "/Users/you/project",
  "workspaceRoot": "/Users/you/project",
  "permissionMode": "default",
  "toolName": "run_terminal_command",
  "toolInput": { "command": "npm test" },
  "timestamp": "2026-04-14T12:00:00Z"
}

כל אירוע נושא את אותם שדות משותפים: hookEventName, sessionId, cwd, workspaceRoot, timestamp, permissionMode (default, auto, plan, או bypassPermissions), ו-promptId (התור שהאירוע שייך אליו. נעדר באירועים בהיקף סשן ובאירועי כלים), ועוד שדות ייחודיים לאירוע כמו toolName למעלה. כשסביבת ריצה חיצונית מריצה את הכלים, cwd של אירוע כלי הוא הספרייה שבה הצעד רץ. ה-cwd של קריאת מעטפת עצמה בא ראשון. הוא נקרא כטקסט בלי חיפוש בדיסק, כפי שסביבת הריצה מריצה אותו. כתובת file:// נקראת כנתיב שלה, ו-~ מוביל כספריית הבית. cwd יחסי מתחבר לספריית הסשן. .. משמיט את השם שלפניו, גם כשהשם הזה הוא symlink. המעטפת של גרוק עצמו שולחת את ספריית הסשן. ספרייה מעל 4,096 בתים לא נפתרת. ה-cwd של האירוע הוא אז זה של הסשן. קריאת מעטפת עדיין שומרת את ה-cwd שלה ב-toolInput. חיפוש בלי נתיב מקבל את ה-cwd שנשלח כנתיב שלו. המפתח hook_event_name (מפתח snake_case) נושא את ערך ה-PascalCase של Claude. המפתח hookEventName (מפתח camelCase) נושא את ערך ה-snake_case של grok.

קלט כלי שה-JSON שלו עובר 128 KiB שומר כל מפתח. המחרוזות שלו שארוכות מאורך משותף אחד נחתכות אליו, כל אחת מסתיימת בתג קצר של הערך השלם שלה וב- [truncated], ו-toolInputTruncated הוא true. האורך הזה הוא הארוך ביותר שנכנס ב-128 KiB אבל אף פעם לא מתחת ל-4 KiB, כך שקלט יכול להישאר מעבר ל-128 KiB אחרי החיתוך שלו. מחרוזת של 4 KiB או פחות אף פעם לא נחתכת, כך ששדה כמו file_path נשאר שלם, וקלט בלי מחרוזת ארוכה יותר נשלח שלם עם toolInputTruncated מוגדר ל-false. מחרוזת חתוכה מסתירה את סופה, כך ששומר שבודק את הטקסט של מחרוזת צריך לשלול אלא אם toolInputTruncated הוא false. כש-toolInputTruncated הוא true, מחרוזת חתוכה ש-updatedInput מחזיר בלי שינוי במקום שלה רצה שלמה, גם לצד מחרוזת אחרת שמתחילה באותו אופן. מחרוזת או מפתח ש-updatedInput מוסיף או משנה ועדיין מכילים [truncated] נדחים, וזה מכסה מחרוזת חתוכה שנערכה, נעטפה, או הועתקה או הועברה לשדה אחר, כמו new_string שמוגדר ל-old_string. המרה שמסירה או מקודדת את הסימון רצה כפי שנכתבה, כמו כל החלפה אחרת.

#פלט (הוקים חוסמים)

להוקי PreToolUse, כתבו JSON ל-stdout:

  • אפשר: {"decision": "allow"}
  • שלול: {"decision": "deny", "reason": "Unsafe command detected"}
  • שאלו את המשתמש: {"decision": "ask", "reason": "Confirm this deploy"}
  • אין עמדה: {"decision": "defer"}
  • שכתבו את קלט הכלי: {"hookSpecificOutput": {"hookEventName": "PreToolUse", "updatedInput": {"command": "npm test"}}}
  • אמרו למודל משהו: {"hookSpecificOutput": {"hookEventName": "PreToolUse", "additionalContext": "This repo builds with xb, not cargo"}}
  • עצרו את הסוכן: {"continue": false, "stopReason": "The build is red"}. הקריאה לא רצה והתור מסתיים, תהיה ההחלטה אשר תהיה. קריאות מוקדמות יותר באותה תשובה שכבר אושרו עדיין רצות, ומאוחרות יותר מדולגות. אתם רואים את stopReason. המודל לא רואה אותו. פרומפטים בתור ממתינים אז, ושום משימה, תת-סוכן או יקיצה של המתזמן לא מפעילה מחדש את הסוכן, עד שתשלחו פרומפט. עצירה של תת-סוכן מסיימת רק את התור שלו. כשסביבת ריצה חיצונית מריצה את הכלים, הקריאה נשללת אבל התור ממשיך.

אפשר לכתוב את ההחלטה כ-decision ברמה העליונה או כ-hookSpecificOutput.permissionDecision. שניהם מקבלים allow, deny, ask, או defer (הכתיבים הישנים approve ו-block גם עובדים), ולכל אחד שדה סיבה משלו: reason ו-permissionDecisionReason. ה-permissionDecision הקנוני מכריע כשהוא נוכח. ה-decision ברמה העליונה חל רק כשהוא חסר. הודעת השלילה או השאלה היא permissionDecisionReason אם הוא נוכח, אחרת reason. allow אומר רק "לא חסום": הוא לא מאשר אוטומטית קריאה שהמשתמש היה נשאל עליה אחרת. ערך החלטה מחוץ לקבוצה הזו הוא כשל הוק, שנכשל פתוח אלא אם ההוק גם יוצא עם 2, ובמקרה הזה השלילה נשארת בתוקף ונושאת את הטעות בסיבה שלה.

תשובת allow, ask או defer לא מסירה שלילת יציאה 2. ביציאה 2, הסיבה של תשובת deny מחליפה את stderr כהודעת השלילה. Claude Code מתנהג באותו אופן.

ask מביא את הקריאה להנחיית ההרשאה: שום דבר שהיה מאשר אותה בלי שאלה, מצב אישור תמיד, מצב אוטומטי, הרשאת "always allow" שמורה, פקודה בטוחה, לא חל, וההנחיה מציינת את ההוק שלכם ומציגה את הסיבה שלכם. אף פעם אין הנחיה שנייה: במקום שבו הייתם נשאלים ממילא, ה-ask רק נותן תווית חדשה לאותה הנחיה. אישור מריץ אותה. דחייה חוסמת אותה כדחיית הרשאה רגילה. לקוח שרץ במצב אישור תמיד מלא/YOLO (שעונה אוטומטית על כל הנחיה) עדיין יאשר את הקריאה אוטומטית, בהתאמה ל-bypassPermissions של Claude Code: ה-ask דורס את נתיבי האישור תמיד, האוטומטי, ההרשאה השמורה והפקודה הבטוחה של המנהל, לא לקוח שמאשר כל הנחיה באופן גורף.

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

רק הוקים שמוגדרים בקובץ הגדרות (הוקי פקודה ו-HTTP) יכולים לשאול, לדחות, או לשלוח additionalContext: הוק PreToolUse שנרשם דרך grok-agent-sdk יכול לאפשר או לשלול, והשאר נזרק. ask או defer שם משאירים את הקריאה לזרימת ההרשאה הרגילה ונרשמים כהחלטה לא מזוהה, ו-additionalContext אף פעם לא מגיע למודל.

updatedInput מחליף את קלט הכלי לפני שהוא רץ, בשקט: לא אומרים למודל ושום דבר לא נכתב ל-scrollback, כך שהסימן היחיד לשכתוב הוא הארגומנטים המשוכתבים עצמם, שהמשתמש רואה אם הקריאה מגיעה להנחיית הרשאה. שער מצב התוכנית, הנחיית ההרשאה, הכלי עצמו, והמטען המאוחר של PostToolUse כולם רואים את הקלט המשוכתב, כך שהוק יכול לנרמל או להקשיח קריאה ולא רק לאפשר או לשלול אותה. הוקים רצים לפני שער מצב התוכנית, כך שהוק עם תופעות לוואי מופעל גם כשמצב תוכנית דוחה אחר כך את הקריאה.

הערך חייב להיות אובייקט JSON. ערך שאינו אובייקט מכשיל את ההוק. אם הקלט המשוכתב נכשל בסכמת הכלי, הקריאה נחסמת כשלילת הוק, ההערה ב-scrollback מציינת את ההוק, והקריאה לא נופלת חזרה למקור. שכתוב יכול לשנות ארגומנטים של קריאה אבל לא איזה כלי רץ, כך ששכתוב שמכוון מחדש קריאת use_tool נחסם גם הוא. הוק שיוצא עם קוד שאינו אפס שומר את ה-deny שלו אבל מאבד את ה-updatedInput ואת ה-additionalContext שלו.

deny משליך כל updatedInput. כשכמה הוקים מחזירים אחד, האחרון מנצח. השמטת decision תוך החזרת updatedInput מאפשרת את הקריאה ומחילה את השכתוב.

defer לא חוסם את הקריאה ולא מאשר אותה: הקריאה עוברת את זרימת ההרשאה הרגילה, בדיוק כאילו ההוק שלכם לא ענה, ואזהרה שמציינת את ההוק נכתבת ללוג. הוא גם לא פועל על שום דבר אחר ששלחתם: updatedInput או additionalContext ליד defer מתעלמים מהם והם מצוינים בלוג. בין הוקים defer מדורג מתחת ל-ask, כך שבמקום שאחד מההוקים שלכם דוחה ואחר שואל, grok מציג הנחיה.

additionalContext הוא הערה למודל. הוא מגיע עם תוצאות האצווה שהקריאה שייכת אליה, עטוף בתג התזכורת של ה-harness שלכם (<system-reminder> כברירת מחדל) ומציין את ההוק שכתב אותו, כך שהמודל יכול להבדיל את הטקסט שלכם מטקסט המשתמש. כל הוק ששולח אחד נמסר, לפי הסדר שבו ההוקים רצו (בניגוד ל-updatedInput, שבו הכותב האחרון מנצח). קריאה שכלל שלילה [permission] דוחה עדיין מוסרת אותו עם תוצאות האצווה. deny של הוק משליך את כולו ומציין את ההשלכה בלוג. קריאה שנדחית בהנחיית האישור משליכה אותו גם. טקסט מעל 10,000 תווים נחתך, אותה תקרה שמשוב Stop נושא.

#פלט PostToolUse

PostToolUse רץ אחרי שהכלי סיים, ולכן הוא לא חוסם דבר. ה-stdout שלו עדיין נקרא, כי הוא קובע מה המודל רואה בהמשך. כתוב JSON ל-stdout:

{
  "decision": "block",
  "reason": "The diff still contains a debug print",
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": "This file is generated; edit the template instead",
    "updatedToolOutput": { "type": "Bash", "command": "…", "exit_code": 0, "output_for_prompt": "[redacted]" }
  }
}
שדההשפעה
decision: "block" + reasonמעביר את reason למודל ליד תוצאת הכלי. הפלט של הכלי עצמו עדיין מגיע. "block" פירושו "אמור למודל שמשהו השתבש", ולא "עצור את הקריאה".
additionalContextמוסיף הערה למודל ליד תוצאת הכלי.
updatedToolOutputמחליף את העותק של התוצאה אצל המודל. מפתח אוניברסלי. עובד לכל כלי.
updatedMCPToolOutputכינוי ל-MCP בלבד של updatedToolOutput. מתעלמים ממנו בכלי מובנה.
continue: false + stopReasonמסיים את התור אחרי שה-hooks של הקריאה רצו. התוצאה נשארת. אתה רואה את stopReason, והמודל לא. הנחיות בתור ו-wakes ממתינים אז לך, כמו אחרי עצירה של PreToolUse. עצירה של subagent מסיימת רק את התור שלו. כש-runtime חיצוני מריץ את הכלים, התור ממשיך.
  • מסירה. סיבת ה-block ו-additionalContext מגיעים אחרי תוצאת הכלי, עטופים בתג התזכורת של ה-harness שלך ומציינים את ה-hook שכתב אותם, כדי שהמודל יוכל לפעול באותו תור. סיבת ה-block ו-additionalContext של כל hook נמסרים לפי הסדר שבו ה-hooks רצו, כך שממצא של hook אחד לא יכול להשמיט ממצא של אחר. רק החלפות פועלות לפי הכלל שהכותב האחרון מנצח: כששני hooks מחזירים אחת, האחרונה נשארת וההשמטה מצוינת בלוג. החלפה שהמודל היה קורא באותו אופן כמו התוצאה שנמסרה ל-hook היא echo. echo הוא אותו ערך, ערך שמתרנדר לאותו טקסט בכלי מובנה, או הטקסט הזה כמחרוזת דרך מפתח שהכלי מקבל. echo לא משנה דבר ולא דוחק החלפה קודמת של hook. תוצאה שמביאה תמונות שהטקסט שלה משמיט (קריאת תמונה או PDF, או קריאת קובץ או תוצאת MCP ש-grok הסיר מהן תמונות לפני שה-hook רץ, אלא אם harness של טקסט בלבד משמיט אותן) נחשבת רק לאותו ערך. כשנותנים תוצאה כזו כטקסט, שליחת הטקסט הזה בחזרה משמיטה את התמונות. זה מכסה תוצאה גדולה ואת טקסט השגיאה של כשל. זה תקף לכל כלי, כולל MCP, ול-PostToolUseFailure.
  • בניית updatedToolOutput. בכל כלי, מחרוזת JSON הופכת לטקסט שפונה למודל כלשונה. hook של השחרה מסווה את הסודות בטקסט ושולח אותו בחזרה כמחרוזת. בכלי מובנה, כל ערך אחר חייב לשאת את צורת הפלט של grok לכלי, אובייקט מתויג כמו {"type": "Bash", …}, שהמודל קורא כפי שהכלי מרנדר אותו. קח את ה-toolResult שהאירוע מסר לך, ערוך אותו, ושלח אותו בחזרה. כש-runtime חיצוני מריץ את הכלים, output_for_prompt של תוצאת shell הוא פלט הכלי ב-JSON של אותו runtime, ולא הסיכום exit: N… של grok או הטקסט שהמודל שלו קרא. תוצאה ערוכה מגיעה למודל כפי ש-grok מרנדר Bash. ייתכן ש-truncated ו-total_bytes חסרים. ייתכן ש-output_file ריק. current_dir הוא הספרייה שה-runtime דיווח, ואחרת ספריית הקריאה. הוא ריק רק כשאף אחת מהן לא ידועה. כש-runtime חיצוני מריץ את הכלים, pre_formatted של shell ברקע מחזיק את הטקסט של אותו runtime. כל עוד pre_formatted מוגדר, המודל קורא אותו ולא אף אחד מהשדות האחרים של ה-shell. החלפת Bash יכולה להשמיט את truncated ו-total_bytes או לשלוח אותם כ-null. כשהם חסרים או null, הם נקראים כ-false ו-0. החלפה מובנית שאינה פלט כלי של grok מתעלמים ממנה והמקור נשאר, אבל הריצה של ה-hook נרשמת Failed עם הסיבה, כך ש-hook עם יציאה 0 שמראה "failed" מדווח על החלפה שהושמטה, ולא על כך שהוא מעולם לא רץ. decision שגוי בהקלדה (רק "block" מכובד) מדווח באותו אופן.
  • תוצאות גדולות. ה-toolResult עם הטיפוס נשלח כל עוד ה-JSON שלו נכנס ב-128 KiB. גדול ממנו מוחלף בכל הטקסט שפונה למודל של הכלי כמחרוזת JSON, ו-toolResultTruncated הוא true. הטקסט הזה הוא הטקסט שפונה למודל של הכלי אחרי מגבלות הפלט של grok לכל כלי. כש-runtime חיצוני מריץ את הכלים, זה פלט הכלי הטקסטואלי של אותו runtime. hook של השחרה רואה כל בייט שלו. החזר אותו כ-echo במחרוזת. בקריאת תמונה או PDF, הטקסט הזה משמיט את התמונה. שליחתו בחזרה משמיטה את התמונה.
  • כלי MCP. לכלי MCP אין צורת פלט. לא updatedToolOutput ולא updatedMCPToolOutput נבדקים מולה. מחרוזת JSON הופכת לטקסט שפונה למודל כלשונה. כל ערך אחר עובר serialization. ה-hook האחרון שכותב מנצח על פני שני המפתחות.
  • תקרות. סיבת ה-block ו-additionalContext נחתכים ב-10,000 תווים, התקרה שמשוב Stop וההקשר של PreToolUse חולקים. החלפה יכולה להיות ארוכה עד 64 K תווים יותר מהטקסט שפונה למודל שהיא מחליפה. עותק מושחר של פלט גדול לעולם לא נחתך. התקרות נמדדות על הטקסט המרונדר שפונה למודל ומוחלות אחרי שההחלפה רונדרה, כך ש-updatedToolOutput ארוך נחתך כמו מחרוזת, ולא מושמט בגלל הגודל. החלפה מובנית מושמטת רק כשהיא אינה פלט כלי של grok.
  • hook שבור. יציאה שאינה אפס, כולל יציאה 2, שומרת את סיבת ה-block ומשמיטה את כל השאר: additionalContext וההחלפה מושמטים וההשמטה מצוינת בלוג, אותו כלל ש-PreToolUse מחיל על updatedInput. ה-block הוא כיוון ה-fail-safe.
  • רשומה מול מודל. החלפה משכתבת רק את העותק של המודל. ה-scrollback, התמליל והטלמטריה שומרים את המקור, כך שהשחרת סוד מסתירה אותו מהמודל, לא ממך, וכשל שנכתב מחדש כהצלחה נשאר אמיתי ברשומה. תמונות לא נמסרות תחת החלפה, כך שצילום מסך או קריאת PDF שהוחלפו מגיעים למודל כטקסט שלך בלבד. כדי להשמיט תמונה, שלח מחרוזת או הגדר את model_text של התמונה. תוצאת תמונה שנשלחת בחזרה עם נתונים מרוקנים עדיין מרנדרת את הכיתוב ברירת המחדל שלה. הכיתוב אומר שהתמונה מצורפת. קריאת PDF משמיטה את העמודים רק דרך מחרוזת. עמודים מרוקנים עדיין מתרנדרים כעמודים מרונדרים. כל מה ש-hook שולח (הערה, סיבת block, החלפה) עובר escaping כדי שלא יוכל לסגור את תג התזכורת ולהתחפש להוראה שחיברו harness או משתמש.
  • החלפת פלט היא מקובץ ההגדרות בלבד. hooks של פקודה ושל HTTP יכולים לעשות את כל זה. hook של PostToolUse שנרשם דרך grok-agent-sdk יכול לתרום סיבת block ו-additionalContext, אבל לא יכול להחליף את פלט הכלי.
  • מתי הוא נורה. PostToolUse נורה לכל כלי שבאמת רץ, כולל כלי שהתוצאה שלו היא שגיאה לוגית מובנית כמו יציאה שאינה אפס של run_terminal_command. כלי שנכשל ב-dispatch, או כלי MCP שהחזיר תוצאת שגיאה, מפעיל במקום זאת את PostToolUseFailure. ה-hook הזה יכול להזין למודל additionalContext, להחליף את טקסט השגיאה, ולסיים את התור עם continue: false, אבל הוא לא יכול לחסום. שדה error של האירוע הוא הטקסט שהחלפה עומדת במקומו. updatedToolOutput או updatedMCPToolOutput מחליפים אותו: מחרוזת JSON כלשונה, כל ערך אחר עובר serialization. ה-hook האחרון ששולח אחד מנצח. כשל dispatch עדיין מגיע למודל כ-Tool `<name>` failed: <your text>. ההחלפה הזו קורית במסלול ה-shell בלבד. במסלול ה-daemon מתעלמים מהחלפת PostToolUseFailure, כמו ב-Claude Code 2.1.316, והמודל קורא את השגיאה של הכלי עצמו. אותן תקרות, escaping, וכללי רשומה מול מודל חלים. ה-hook יורש את ברירת המחדל של שער 600 שניות (הוא בדרך כלל מריץ linter או בדיקה). הגדר timeout במפורש רק כשהבדיקה צריכה יותר או פחות. hook שפג זמנו נרשם ככשל ולא תורם דבר.

#קודי יציאה

קוד יציאהמשמעות
0הצלחה / allow (ל-hooks חוסמים)
2דחייה מפורשת (PreToolUse, אלא אם ה-JSON ב-stdout אומר continue: false, מה שעוצר את הסוכן), block-stop עם stderr כמשוב (Stop/SubagentStop, כש-stdout לא מחזיק תשובה קריאה), או משוב למודל (PostToolUse). עבור PreToolUse, שורת stderr הראשונה (חתוכה) הופכת לסיבת הדחייה כשה-JSON לא נושא סיבה. Stop/SubagentStop ו-PostToolUse מזינים את כל ה-stderr למודל, ו-reason ב-JSON גובר עליו.
אחרFail-open: הכשל נרשם (כ-exit code N: <first stderr line>) אבל דבר לא נחסם. handler עם "failClosed": true הוא החריג: הכשל שלו דוחה את הקריאה (PreToolUse) או חוסם את ההנחיה (UserPromptSubmit). תשובת PreToolUse או UserPromptSubmit מעבר למגבלת קריאה דוחה או חוסמת (ראה קריאת תשובה של Hook). עבור PreToolUse, החלטת deny או continue: false ב-JSON של stdout מכובדת בלי קשר לקוד היציאה. עבור Stop/SubagentStop, תשובה קריאה ב-stdout מחליטה בכל קוד יציאה, כולל יציאה 2. תשובה שלא מגדירה block נותנת לתור להסתיים בלי להזין stderr בחזרה. קוד היציאה מחליט רק כש-stdout לא מחזיק תשובה קריאה. אז יציאה 2 חוסמת עם stderr כמשוב. עבור PostToolUse, הכלי כבר רץ ולכן דבר לא נחסם כך או כך. הכשל עדיין נרשם, וה-hook שומר את סיבת ה-block שלו ואת continue: false שלו אבל מאבד את additionalContext ואת החלפת הפלט.

יציאה 2 של PostToolUse היא שינוי התנהגות. פעם זה היה כשל רגיל שנרשם ולא שינה דבר. עכשיו זה מזין את ה-stderr של ה-hook למודל. hook לרישום שנכתב כ-run_checker; exit $? מוסר למודל כל מה שהבודק הדפיס בכל פעם שהבודק יוצא 2. mypy, grep, pytest ו-argparse כולם משתמשים ביציאה 2 עבור "no match" או "bad usage", וב-Windows robocopy יוצא 2 כשהוא מוצא קבצים נוספים ו-net בכל שגיאה. סיים hook כזה ב-exit 0 מפורש כדי להשאיר אותו שקט.

כתוב אבחונים קריאים לאדם ל-stderr: זה ערוץ המשוב של ה-hook. בכשלים שורת stderr הראשונה מופיעה ברשומת ה-scrollback ובלוגים במקום קוד יציאה חשוף.

#PowerShell ב-Windows

תחת pwsh או Windows PowerShell, Grok מוסיף שורות יציאה אחרי פקודה מוטמעת כדי שהיא תצא כפי שהייתה יוצאת תחת sh -c במקום שבו הוא יכול:

  • פקודה שהפקודה האחרונה שלה היא תוכנית יוצאת עם הקוד של אותה תוכנית, כך שתוכנית כמו node guard.js שיוצאת 2 עדיין חוסמת.

  • כשהפקודה האחרונה היא פקודת PowerShell שנכשלת, ה-hook יוצא עם קוד היציאה של התוכנית האחרונה אם הוא לא היה 0, ואחרת 1. זה שונה מ-sh -c ונוטה לכיוון חסימה. בלי stale.lock, grep -q x missing.txt; Remove-Item stale.lock יוצא 2, בעוד grep -q x missing.txt; rm stale.lock תחת sh יוצא 1. זה דוחה PreToolUse, חוסם Stop ומזין את ה-stderr של hook של PostToolUse למודל.

  • שגיאה מסיימת מסיימת את ה-hook ביציאה 1, מה שרץ לפניה. זה throw, וכל שגיאה תחת $ErrorActionPreference = 'Stop' או -ErrorAction Stop, כך ש-$ErrorActionPreference = 'Stop'; node guard.js; Remove-Item stale.lock לא חוסם כשאין stale.lock.

  • תחת $PSNativeCommandUseErrorActionPreference = $true ו-$ErrorActionPreference = 'Stop', תוכנית שהיציאה שלה עוצרת את ה-hook שומרת גם את קוד היציאה שלה, כמו תחת set -e. תחת pwsh, Grok מוסיף את ה-trap הזה אחרי הפקודה לשם כך:

    trap { if ($_.FullyQualifiedErrorId -eq 'ProgramExitedWithNonZeroCode') { exit $_.Exception.ExitCode }; break }

    הוא מוסיף את ה-trap רק כשכל התנאים הבאים מתקיימים. כשקוד רץ תחת העדפה אחרת, ה-trap היה מסיים את ה-hook בשגיאה שה-hook היה ממשיך מעבר לה, כמו אחת לפני exit 2 שלו.

    • הפקודה נפתחת בשתי ההגדרות האלה, בכל סדר, ועם הערכים המילוליים $true ו-'Stop' (או "Stop").
    • רק Set-StrictMode -Off, או Set-StrictMode -Version עם Latest, 1 עד 5 (עם או בלי .0) או 5.1, בא לפניהן, כמקשה אחת בשורה אחת. הערה יכולה לסיים את השורה הזו, אבל רק שורות ריקות באות לפניה, אפילו לא הערה או #requires -Version 7.3. כל PowerShell מקבל את הגרסאות האלה. גרסה שחלק מ-PowerShell דוחים, כמו 3.5 או 7.4, הייתה שגיאה שה-trap מסיים בה את ה-hook.
    • רק רווחים והערה באים בין הגדרה ל-; שלה או למעבר השורה.
    • השורה הבאה אחרי ההגדרות, מעבר לשורות ריקות ולהערות, לא מתחילה ב-|, מה שהיה ממשיך את ההגדרה השנייה.
    • דבר אחריהן לא מחזיק errora, כמו ב--ErrorAction, או את המילה ea, אפילו בנתיב כמו C:\Users\ea. וגם לא מזכיר trap, iex, Invoke-Expression, scriptblock, InvokeScript, Import-Module, ipmo, #requires, שיכולים לטעון מודול, או קובץ .ps1, .psm1 או .psd1. זה כולל את pwsh -File guard.ps1, אף שתהליך בן לא יכול לשנות את ההעדפה.
    • אין ערך ב-env של ה-hook או של התוסף שלו שמציין קובץ .ps1, .psm1 או .psd1. ערכים אחרים יכולים להזכיר כל דבר.

    סקריפט או מודול שהפקודה מגיעה אליו בלי לציין את הקובץ שלו עדיין יכול להגדיר העדפה משלו, וה-trap יכול אז לקטוע אותו בשגיאה שהוא היה ממשיך מעבר לה. דוגמאות הן .\guard שמריץ את guard.ps1, מודול ש-PowerShell טוען בעצמו, או $env:GUARD שה-hook יורש ולא מגדיר ב-env.

    אחרת, תוכנית שעוצרת את ה-hook מסיימת אותו ביציאה 1. עטוף את התוכנית ב-try { ... } catch { exit $LASTEXITCODE } כדי לשמור את הקוד שלה. ההגדרה סטנדרטית מ-pwsh 7.4 וניסיונית ב-7.3. ל-Windows PowerShell 5.1 אין אותה.

  • פקודה שמורכבת מבלוקים begin, process, end, clean (pwsh 7.3 ואילך) או dynamicparam רצה ככתוב, כי PowerShell לא מתיר דבר אחריהם, וכך גם hook של .ps1. אף אחד מהם לא שומר בעצמו יציאה 2 של תוכנית. שים exit $LASTEXITCODE בתוך הבלוק האחרון, כי exit אחרי הבלוקים נכשל בפענוח. תחת Set-StrictMode, קריאה של $LASTEXITCODE לפני שתוכנית כלשהי הגדירה אותו היא שגיאה, שהופכת לשורת stderr הראשונה. השתמש שם ב-exit (Get-Variable LASTEXITCODE -ValueOnly -ErrorAction Ignore), כמו ששורות היציאה עושות. תחת שתי ההגדרות למעלה, פקודה כזו או hook של .ps1 צריכים גם עותק משלהם של ה-trap למעלה, או try/catch, כדי לשמור את הקוד של תוכנית שעוצרת אותם.

  • Grok מזהה פקודה כזו לפי איך היא מתחילה, כמו ש-PowerShell עושה. הוא עובר על הצהרות using, תכונות כמו [CmdletBinding()] ובלוק param(...), ומתאים את הסוגריים שלהם מעבר למחרוזות ולהערות. רווחים, מעברי שורה, הערות (כולל #requires) והמשכי שורה ב-backtick יכולים לבוא לפני החלקים האלה וביניהם. הפקודה רצה ככתוב רק כשאחת מהמילים האלה באה אחר כך, בכל רישיות, ואז {. כל פקודה אחרת מקבלת את שורות היציאה, כך ש-[Console]::Error.WriteLine('end { }'); node guard.js שומר את יציאה 2 של node. כמו ב-PowerShell, הערה או backtick שצמודים למילת מפתח הופכים אותה לחלק ממילה ארוכה יותר, כך ש-end<# c #>{ ... } מקבל גם הוא את שורות היציאה, בעוד end <# c #> { ... } רץ ככתוב.

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

  • Windows PowerShell 5.1 מריץ את בלוק process של פקודה פעם אחת לכל שורה שהוא מעביר מ-stdin, והוא מעביר את stdin רק כשהפקודה קוראת את $input שלה, כפי שגם @input ב-splat עושה, כך שבלעדיו הבלוק לעולם לא היה רץ. $input בפונקציה או בבלוק סקריפט שהפקודה מגדירה, כמו ForEach-Object { $input }, קורא את הקלט של אותו אחד ולא נספר. שם Grok מריץ פקודה כזו בתוך & { ... }, אחרי הצהרות using שלה, כך שהבלוק רץ פעם אחת, כמו תחת pwsh, ויכול לקרוא את הקלט של ה-hook עם [Console]::In.ReadToEnd(). 5.1 מריץ את בלוק process של hook מסוג .ps1 באותו אופן תחת -File, אז שים את העבודה של hook כזה בבלוק end.

  • סקריפט מתקדם, אחד עם [CmdletBinding()] או פרמטר [Parameter()], לא מקבל דבר ב-$input אלא אם לאחד הפרמטרים שלו יש ValueFromPipeline. PowerShell לא יכול לאגד אליו את הקלט של ה-hook, כך ש-$input נשאר ריק והסקריפט ממשיך ליציאה 0. Grok עדיין מריץ פקודה כזו שקוראת את $input שלה, כי דחייה שהיא עושה בלי הקלט עדיין חוסמת. הוא מוסיף אזהרה ל-systemMessage של ה-hook, אחרי כל אזהרה משלו, גם כשה-hook פג זמנו או אי אפשר לקרוא את הפלט שלו, כך שבכל מקום שהאירוע מציג אחת, guard שמאפשר מחוסר הקלט שלו לא עובר בלי להיראות. דחיית JSON שומרת את הסיבה שלה, בעוד exit 2 חוסם עם שגיאת האיגוד של PowerShell כסיבה שלו, השורה הראשונה ב-stderr. דחייה בבלוק process לעולם לא רצה, כי PowerShell מדלג על הבלוק הזה לקלט שהוא לא יכול לאגד, אז שים אותה בבלוק end. קרא את הקלט עם [Console]::In.ReadToEnd(), גם ב-hook של .ps1, ש-Grok מריץ בלי הבדיקה הזו.

  • כשהטקסט משאיר לא ברור אם $input הוא של הפקודה עצמה, Grok לא מריץ את הפקודה בתוך & { ... } תחת Windows PowerShell 5.1. זה $input ב-{ שפותח סעיף switch, בערך של מאפיין של מחלקה, או אחרי פקודה ש-pwsh עשוי לקרוא את שמה כמספר, כמו 1u# או 0b1#. תחת 5.1, בלוק process של פקודה כזו לעולם לא רץ. בסקריפט מתקדם, Grok מציג גם עבורה את האזהרה למעלה. קרא את הקלט עם [Console]::In.ReadToEnd() גם שם.

#קריאת תשובה של Hook

Grok קורא תשובה שמתחילה ב-{ כמו ש-JSON.parse של JavaScript עושה. סימן סדר בתים מוביל מושמט. האחרון מבין שני מפתחות כפולים מנצח. בריחת surrogate בודדת של UTF-16 הופכת ל-U+FFFD. מספר גדול מדי ל-double הופך לגדול ביותר. כל דבר אחר הוא טקסט רגיל.

  • לא JSON תקין. לאירוע שקורא את תשובת ה-hook שלו, תשובה שמתחילה ב-{ אבל אינה JSON תקין מכשילה את הריצה ביציאה 0. כך גם תשובה שלא תואמת את שדות האירוע ("decision": 5). הכשל מוצג. ה-hook נכשל פתוח. בכל קוד יציאה אחר קוד היציאה מחליט. ריצה של HTTP hook נכשלת יהיה הסטטוס אשר יהיה. אירועי תצפית בלבד, כמו SessionStart, Notification ו-PreCompact, קוראים מהתשובה רק systemMessage ברמה העליונה. systemMessage של hook מסוג StopFailure, PreCompact, PostCompact או SessionEnd מושמט, כמו ב-Claude Code.
  • מגבלות קריאה. תשובה נקראת עד 8 MiB, תוך בניית לכל היותר 524,288 ערכים ומפתחות, בכל עומק. תשובה ארוכה מ-8 MiB עדיין נקראת כאשר אחריה רק רווח לבן. כאשר טקסט אחר בא אחריה, היא מעבר למגבלה רק אם 8 MiB הראשונים שלה עדיין יכולים להתחיל מסמך ארוך יותר. אחרת זה טקסט רגיל או JSON שאינו תקין.
  • מעבר למגבלה. Grok לא יכול לדעת מה החליטה תשובה שעברה מגבלה. hook מסוג PreToolUse אז דוחה את קריאת הכלי, יהיה הסטטוס של HTTP hook אשר יהיה. hook מסוג UserPromptSubmit חוסם את ההנחיה. הגוף של HTTP hook מסוג UserPromptSubmit נספר רק בסטטוס 2xx. הסיבה היא hook reply was not read: <limit>. command hook שיוצא ב-2 מוכרע לפי יציאה 2 כרגיל. Stop, SubagentStop, PostToolUse ו-PostToolUseFailure מתייחסים לזה כמו לתשובה שאינה JSON תקין. הריצה נכשלת ביציאה 0 ונכשלת פתוח. בכל קוד יציאה אחר קוד היציאה מחליט. אירועי תצפית בלבד לא מקבלים ממנה systemMessage.
  • קינון בערכים ש-Grok מעביר הלאה. updatedInput, updatedToolOutput ו-updatedMCPToolOutput מועברים בשלמותם עד 124 רמות. בעומק הזה עדכון סשן שלם או הודעת ACP שנושאת אחד נשארים בתוך מה שלקוח ACP כמו ה-pager מפענח. updatedInput עמוק יותר על allow, על ask או על תשובה בלי החלטה, מ-hook שהצליח (יציאה 0, או סטטוס 2xx), דוחה את קריאת הכלי עם hook reply was not read: `updatedInput` nests more than 124 levels deep. מ-hook שנכשל (קוד יציאה שאינו 0 או 2, או סטטוס שאינו 2xx), השכתוב מושמט והקריאה ממשיכה. החלפת פלט עמוקה יותר מ-hook שהצליח מונעת את פלט הכלי. המודל קורא Tool output withheld: `updatedToolOutput` nests more than 124 levels deep (או updatedMCPToolOutput) במקומו. הריצה נרשמת כנכשלת. סיבת החסימה שלה ו-additionalContext עדיין חלים.

#בקרת החלטת Stop

hooks מסוג Stop ו-SubagentStop רצים כשהסוכן עומד לסיים את התור שלו ויכולים להשאיר אותו עובד (תואם Claude Code). כתבו JSON אל stdout:

  • חסימת העצירה: {"decision": "block", "reason": "The test suite hasn't been run yet"}. הסיבה מוזנת חזרה למודל כהודעת משתמש והסוכן מריץ סבב נוסף באותו תור.
  • משוב שאינו שגיאה: {"hookSpecificOutput": {"hookEventName": "Stop", "additionalContext": "Run the linter before finishing"}}. גם משאיר את הסוכן עובד, אבל מוצג כמשוב hook ולא כשגיאת hook.
  • עצירה כפויה: {"continue": false, "stopReason": "Budget exhausted"}. מסיים את התור, וגובר על כל חסימה.
  • אישור העצירה: יציאה 0 בלי פלט (או כל פלט שאינו JSON).

יציאה עם קוד 2 גם חוסמת את העצירה, עם stderr כמשוב, כאשר stdout לא מחזיק תשובה ש-Grok יכול לקרוא. תשובה שניתן לקרוא מחליטה במקום זה, גם אחת שלא קובעת decision.

קלט ה-hook כולל את stopHookActive ואת lastAssistantMessage. stopHookActive הוא true כאשר הסוכן כבר ממשיך בגלל חסימת stop-hook קודמת בתור הזה. בדקו אותו, או את התמליל, כדי להימנע מחסימה על תנאי שלעולם לא ייפתר. lastAssistantMessage נושא את הטקסט של התגובה הסופית של הסוכן בתור הזה, כך ש-hooks יכולים לפעול לפיו בלי לפענח את התמליל. כל אירוע שנושא את השדה הזה חותך אותו ב-32,768 תווים, עם אותו סמן … [+N chars] כמו שדות הטקסט החופשי האחרים. הוא רופף בהרבה מ-1,000 שחל על errorDetails והשדות הדומים, כי הוא נושא תשובה שלמה ולא תווית, והוא בגודל של אותו סדר גודל כמו תקרת המטען של הכלי. אחרי 8 המשכים (חסימות או משוב שאינו שגיאה) בתור אחד השער נעקף והתור נגמר. לא מתייעצים עם hooks לעצירה הסופית הכפויה הזו. המונה הוא לכל תור: ההנחיה הבאה של המשתמש מתחילה מחדש, כך שמטרה ארוכה יכולה להיפרש על פני תורות. כשלי hook נכשלים פתוח: הסוכן עוצר כרגיל.

ל-hooks מסוג Stop, SubagentStop ו-PostToolUse ברירת מחדל של timeout של 600 שניות, כי השערים האלה בדרך כלל מריצים בניות או חבילות בדיקות, ו-hook שתם הזמן שלו נכשל פתוח, כך שהבדיקה לא חוסמת ממילא. כל אירוע אחר שומר על ברירת המחדל של 5 שניות. קבעו timeout במפורש כששער צריך יותר: { "type": "command", "command": "bin/verify.sh", "timeout": 1200 }.

השער רץ רק להשלמות אמיתיות. תור שהופסק (Ctrl+C), נדחה, או נחתך במגבלת התורות מדלג על שער ה-Stop, אבל Ctrl+C שנוחת בזמן ש-Stop hook כבר רץ הורג אותו באמצע הדרך (ראו למטה). תורות עם שגיאת API מפעילים StopFailure, ותורות שבוטלו מפעילים StopCancelled. Esc אף פעם לא מבטל תור שרץ. Stop נפרד גם נורה בסוף הסשן (reason: "channel_closed" או "shutdown"). הוא רץ באותו זמן כמו ה-hooks של SessionEnd, לכל היותר 1.5s. פלט ההחלטה שלו מפורש אבל מתעלמים ממנו, כי לא נשאר תור להמשיך. סקריפט שסופר יריות Stop או נשען עליהן כשער צריך לבדוק reason == "end_turn" כדי שיריית סוף הסשן לא תטה אותו.

StopFailure הוא תצפית בלבד (השתמשו בו כדי לרשום כשלים או לשלוח התראות. מתעלמים מהפלט ומקוד היציאה). הקלט שלו נושא את error (הסוג המסווג שה-matcher בודק: rate_limit, authentication_failed, invalid_request, server_error, max_output_tokens, או unknown לכל דבר שה-runtime לא יכול להבחין בו. שגיאות קיבולת מסווגות כ-rate_limit), את errorDetails (פירוט השגיאה הגולמי, כשזמין, חתוך ב-1000 תווים. נעדר לסירוב, שההסבר שלו נישא ב-lastAssistantMessage לבדו), את lastAssistantMessage (טקסט השגיאה המרונדר שמוצג בשיחה. לאירוע הזה זו מחרוזת השגיאה, לא פלט העוזר), ואת subagentType (סוג ה-subagent כאשר התור רץ בתוכו).

StopCancelled גם הוא תצפית בלבד. הוא רץ במקום Stop כאשר התור נגמר בלי להשלים, באותו אופן ש-StopFailure רץ במקום Stop בשגיאת API.

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

אם המארח שלכם חייב לא לפספס מעבר למצב idle, האזינו גם ל-Notification של idle_prompt. הוא מכסה כל חריג שבו הסשן עדיין חי, עם פער אחד: סשן שהפעילות היחידה שלו הייתה פקודת bash-mode שרצה עד הסיום לא מקבל לא את הדוח ולא את הפינג, אבל הפרעה לאחת מקבלת את שניהם. SessionEnd מכסה את הפירוק. הפינג idle_prompt נורה כדקה אחרי שהסשן מתייצב, דורש שלפחות תור אחד הסתיים, ומבוטל אם אתם שולחים הודעה אחרת קודם.

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

סקריפט שעוקב אחרי busy ו-idle צריך להשתמש ב-promptId כמפתח, לפי הכללים למטה. grok מנפיק אחד לכל תור, אבל לקוח שמספק משלו ב-_meta הוא הבעלים של הייחודיות שלו, אז התייחסו ל-id כאטום ותחמו אותו לסשן.

כל דוח סוף תור עובר דרך worker אחד, כך ש-hook איטי מעכב את הדוח הבא אבל אף פעם לא את התור שאליו הוא שייך. שמרו את ה-timeouts של observe-hook קצרים.

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

Stop הוא שער, כך שכאשר stop hook חוסם הוא נורה שוב לכל סבב המשך. רק הירייה שנותנת לתור להסתיים היא הדוח, ותור שנגמר כמבוטל או כנכשל אחרי Stop חסום מדווח על זה במקום. צופה פסיבי לא יכול להבחין בין יריית המשך לירייה הסופית (stopHookActive הוא true לשתיהן), כך שממשק ששער רק על Stop מראה idle שגוי מיריית ההמשך הראשונה עד ההנחיה הבאה של המשתמש, כי אין UserPromptSubmit שמסמן סבב המשך. השאירו את Stop מחוץ לסקריפט המצב כאשר אתם גם מריצים שער חוסם, ויישבו לפי Notification של idle_prompt.

חלק מהתורות לא מדווחים על אף אחד משלושת אלה:

  • פקודת bash-mode (!) שרצה עד הסיום. היא לא מעלה hook בכלל: אין UserPromptSubmit, PreToolUse, PostToolUse או Stop של סוף תור, כך ש-hook מסוג PreToolUse לא יכול לדחות אותה. הפרעה לאחת עדיין מדווחת user_interrupt, בלי UserPromptSubmit קודם.
  • פקודת slash מובנית שרצה עד הסיום. הפרעה לאחת עדיין מדווחת user_interrupt, בלי UserPromptSubmit קודם.
  • cancel-and-send, rewind, או הנחיה שהמתינה בתור והוסרה לפני שרצה.
  • פירוק סשן, שמדווח על ידי Stop של סוף הסשן ועל ידי SessionEnd.
  • תור ש-hooks של stop השאירו את הסוכן עובד עד שמגבלת ההמשכים לכל תור כפתה את העצירה.
  • תור שבו אף stop hook לא רץ עד הסוף, כי כולם היו מושבתים, לא מהימנים, או נכשלו, או, ב-subagent, כי כל ה-matchers שלהם החטיאו. השארת התור בלי דיווח היא מכוונת: ביטול או כשל מאוחר יותר יכול אז לדווח אותו.
  • תור שהוחלף על ידי הבא בזמן שהדוח שלו עדיין נבנה.
  • דוח שעדיין בתור כשהסשן יוצא: הפירוק מחכה חצי שנייה ל-hooks של סוף תור שממתינים, ואז משליך את מה שנשאר ומבטל כל hook שעדיין רץ.
  • תור שהושלם, הריץ את ה-Stop שלו, ורק אז נכשל לכתוב לדיסק: הוא דיווח Stop, כך שאחריו אין StopFailure. הכשל עדיין מופיע בשיחה.

הקלט של StopCancelled נושא:

  • reason: הסיבה המסווגת, והערך שה-matcher בודק. user_interrupt (Ctrl+C, כפתור עצירה של לקוח, או session/cancel של לקוח), permission_rejected (דחיתם קריאת כלי), permission_cancelled (סגרתם את הבקשה), max_turns, no_progress (הסוכן יצא אחרי סבבי no-op חוזרים), hook_stopped (continue: false של hook מסוג PreToolUse, PostToolUse או PostToolUseFailure), או unknown (ביטול שה-runtime לא יכול לסווג, וגם נתיב הגיבוי התואם קדימה). ה-matcher בודק רק את השדה הזה, כך ש-hook שרוצה כל עצירה שיזם המשתמש מתאים את הסיבות שאכפת לו מהן וקורא את cancelledBy מהמטען. סיבות חדשות עשויות להתווסף עם הזמן, אז התייחסו לערך לא מוכר כמו שאתם מתייחסים ל-unknown.
  • cancelledBy: user להפרעה, לקריאת כלי שנדחתה, או לבקשה שנסגרה. runtime לכל מה שהסוכן החליט בעצמו, כמו max_turns, no_progress ו-hook_stopped. unknown כאשר reason הוא unknown, כי ביטול שה-runtime לא יכול לסווג לא יכול לטעון שהמשתמש לא היה מעורב. נגזר מ-reason, כך שסיבה חדשה מסווגת אוטומטית. ערכים עשויים להתווסף גם כאן: התייחסו לאחד שאינכם מזהים כמו שאתם מתייחסים ל-unknown, במקום להניח שכל מה שאינו user היה ה-runtime.
  • cancelTrigger: המחווה, כאשר הלקוח נקב באחת, חתוכה ב-64 תווים, כי שם מחווה הוא אסימון. ה-pager המצורף שולח אחת משלוש: ctrl_c, mouse (כפתור העצירה על המסך), או dashboard_stop. הוא אף פעם לא שולח esc (Esc לא מבטל תור). לקוח אחר עשוי לשלוח כל מחרוזת, כולל esc, והיא מועברת כלשונה. כל ערך כאן מסווג כ-user_interrupt, כולל אחד שבמקרה מאיית שם פנימי כמו shutdown, כי לקוח שמבקש לבטל הוא המשתמש שמבקש. קראו את cancelledBy מהמטען במקום לפרק את המחרוזת הזו. מושמט ל-session/cancel חשוף ולכל סיבה שיזם ה-runtime.
  • reasonDetails: אותו סוג פירוט ש-StopFailure שם ב-errorDetails, כאשר ל-runtime יש אחד. לקריאת כלי שנדחתה זה <tool>: <why>. חתוך ב-1000 תווים, כמו errorDetails של StopFailure.
  • lastAssistantMessage: מה שהתור הספיק להתחייב אליו בשיחה ברגע ההפרעה, אם יש. Ctrl+C במהלך התשובה הסופית משאיר את הטקסט האחרון שהתחייבו אליו, או כלום אם התור אף פעם לא התחייב לכלום. חתוך כמו אותו שדה ב-Stop וב-StopFailure.
  • subagentType: סוג ה-subagent כאשר התור רץ בתוכו, כך ש-hook יכול להבדיל עצירה של סוכן מקונן מעצירת הסשן. נעדר בסשן הראשי.

timestamp של המעטפה מוטבע כשה-hook נשלח, לא כשהתור נגמר. שלושת אירועי סוף התור ממתינים בתור מאחורי worker אחד, כך שדוח שמחכה ל-hook איטי לפניו נושא timestamp מאוחר יותר מהרגע שהוא מתאר. השתמשו ב-promptId כדי לקשר, לא בשעון.

StopCancelled לא יכול לחסום: התור כבר נגמר, ומתן אפשרות ל-hook לפתוח מחדש תור שהמשתמש עצר בכוונה היה נלחם במשתמש. השתמשו ב-Stop כשאתם רוצים להשאיר את הסוכן עובד.

"cancel-and-send" (הקלדת הודעה חדשה בזמן שתור רץ) לא מפעיל StopCancelled, כי התור מוחלף, לא נעצר, והסוכן נשאר עסוק. בתוך subagent, user_interrupt גם לא נורה: הוא עוקב אחרי הביטול של ההורה, והאות ברמת הסשן הוא השימושי. max_turns, no_progress, עצירת hook, או הרשאה שנדחתה של ה-subagent עצמו כן נורים. ה-matcher בודק רק את reason, כך שסקריפט שמדווח אם הסשן idle צריך לצאת מוקדם כאשר subagentType נוכח.

מחוון busy ו-idle שלם לוקח חמישה רישומים. UserPromptSubmit מסמן את הסשן כעסוק. Stop, StopFailure ו-StopCancelled מיישבים אותו בכל אופן שהתור נגמר. Notification של idle_prompt הוא הגיבוי לתורות שלא מדווחים על אף אחד משלושת אלה. רישום רק של StopCancelled משאיר את המארח עסוק אחרי כל תור רגיל.

{
  "hooks": {
    "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "bin/turn-started.sh" }] }],
    "Stop": [{ "hooks": [{ "type": "command", "command": "bin/turn-ended.sh", "timeout": 10 }] }],
    "StopFailure": [{ "hooks": [{ "type": "command", "command": "bin/turn-ended.sh" }] }],
    "StopCancelled": [{ "hooks": [{ "type": "command", "command": "bin/turn-ended.sh" }] }],
    "Notification": [
      { "matcher": "idle_prompt", "hooks": [{ "type": "command", "command": "bin/turn-ended.sh" }] }
    ]
  }
}

מה ששני הסקריפטים חייבים לעשות נכון:

  • עקבו אחרי promptId החדש ביותר והתעלמו מדוחות לתורות ישנים יותר. דוח של תור מבוטל נשלח מחוץ ללולאת הפקודות, כך שהוא יכול להגיע אחרי UserPromptSubmit של התור הבא.
  • יישבו ללא תנאי כאשר אין promptId. זה grok שמדווח על הסשן ולא על תור: הפינג idle_prompt ו-Stop של סוף הסשן. זה מה שגורם לגיבוי לעבוד ל-rewind או לתור שהוחלף, שלא מדווחים כלום.
  • התייחסו ל-promptId שמעולם לא ראיתם את ההתחלה שלו כ-idle. תור bash-mode שהופרע מדווח בלי UserPromptSubmit קודם.
  • צאו מוקדם כאשר subagentType נוכח. עצירה של subagent אינה עצירת הסשן.
  • יישבו את המארח לפני שאתם רושמים את התור כטופל, כך ש-hook שנהרג באמצע הדרך משאיר את התור ניתן לתיקון. קראו מחדש את הרשומה הזו קודם, כך שאתם מנקים רק תור שרשמתם בעצמכם.
  • הגבילו את זה לכתיבה מקומית. הפירוק נותן לכל התור של דוחות סוף תור חצי שנייה, אחר כך ל-hooks של Notification שממתינים 1.5s, וכל hook של SessionEnd אז חסום ב- timeout שלו (ברירת מחדל 1.5s. קבעו GROK_SESSION_END_HOOKS_TIMEOUT_MS, באלפיות שנייה, כדי לשנות את ברירת המחדל הזו, עם תקרה של 60s). hook של SessionEnd של תוסף רץ לכל היותר את ברירת המחדל הזו, יהיה ה- timeout שלו אשר יהיה. היציאה ממתינה לכל היותר 10s ל-hooks של SessionEnd בנתיב ה-shell. בנתיב ה- daemon הוא מריץ את ה-hooks של SessionEnd של סשן אחד אחרי השני, כל אחד עד ה-timeout שלו, ומחכה לכל היותר 60s בסך הכל.

Stop הוא שער, כך שהרשומה הזו רצה על הנתיב הקריטי של התור: שמרו אותה מהירה, תנו לה timeout, וצאו 0, כי יציאה 2 חוסמת את העצירה ומשאירה את הסוכן עובד. השאירו את Stop בחוץ אם אתם גם מריצים שער Stop חוסם, כי יריית המשך תיישב את המארח בזמן שהסוכן עדיין רץ. רשמו SessionEnd במקום, והוא הדבר היחיד שמיישב סשן שיוצא לפני הפינג.

שני הסקריפטים גם רצים בתוך הסשן של subagent. כל אירוע שכל אחד מהסקריפטים קורא נושא שם subagentType ומשמיט אותו בסשן הראשי, כך ש-[ -n "$subagentType" ] && exit 0 מסנן ילד משני החצאים. זה הכי חשוב ל-subagent ברקע, שמאריך ימים אחרי תור ההורה ואחרת היה מחזיק את המארח עסוק אחרי שההורה עבר ל-idle.

קלט Stop נושא גם את backgroundTasks ואת sessionCrons, כך שהוק יכול להבחין בין "הסשן הסתיים" לבין "הסשן מושהה וממתין לעבודה ברקע שתעיר אותו בחזרה". שני המערכים ריקים כשאין דבר בביצוע או מתוזמן. כל רשומה ב-backgroundTasks מתארת משימה אחת בביצוע: id, type (shell, monitor או subagent), status, ולפי הסוג גם command (משימות shell בלבד), description (שורת הפקודה שמנטר עוקב אחריה, או תיאור המשימה של subagent), ו-agentType (subagents). כל רשומה ב-sessionCrons מתארת התעוררות מתוזמנת אחת (scheduler_create או /loop): id, schedule, recurring ו-prompt. הערך של schedule הוא מרווח קריא לאדם, למשל every 5 minutes. תזמוני grok הם מרווחים, לא ביטויי cron. שדות טקסט חופשי ברשומה מוגבלים ל-1000 תווים, עם סמן בתוך המחרוזת … [+N chars].

בתוך subagent, השער נורה כ-SubagentStop (הוקי Stop ב-frontmatter של סוכן ממופים מחדש אוטומטית). הוק Stop שולט רק בסוכן הראשי.

SubagentStop נורה פעם אחת לכל subagent, בסוף התור של אותו subagent, בהתאמה ל-Claude Code. הקלט שלו נושא שדה phase (כרגע תמיד "gate") השמור לתאימות קדימה.

העברת הוקי stop של Claude Code: אוצר המילים של הפלט (decision, reason, continue, stopReason, additionalContext) עובד בלי שינוי. בדקו ברשימה הזו מה לא תואם ל-Claude:

  • קלט camelCase עם כינויי snake_case: מעטפת stdin של grok משתמשת במפתחות camelCase, וגם שולחת את האיות snake_case של Claude למפתחות העליונים האלה: session_id, transcript_path, permission_mode, tool_name, tool_input, tool_response, tool_use_id, duration_ms, is_interrupt, stop_hook_active, last_assistant_message, error_details ו-notification_type, כך שסקריפט שקורא את .stop_hook_active עובד בלי שינוי (מפתח snake_case hook_event_name נושא את הערך PascalCase של Claude, למשל "Stop". מפתח camelCase hookEventName נושא את הערך snake_case של grok, למשל "stop"). מפתחות מקוננים ברשומות backgroundTasks ו-sessionCrons נשארים camelCase בלבד, כך שסקריפט שקורא .background_tasks[].agent_type חייב לעבור ל-.backgroundTasks[].agentType. הוקים שנרשמו דרך grok-agent-sdk ממירים גם את המפתחות העליונים וגם את מפתחות הרשומות של backgroundTasks/sessionCrons ל-snake_case, כך ש-.backgroundTasks[].agentType על הקו נקרא כ-.background_tasks[].agent_type ב-SDK.
  • שדה toolResult: פלט הכלי של PostToolUse הוא toolResult (ב-SDK: tool_result). grok גם פולט כינוי snake בשם tool_response שמעתיק את toolResult, כך שהוק שקורא את .tool_response של Claude עובד בלי שינוי.
  • updatedToolOutput בכלים מובנים הוא טקסט או צורת הפלט של grok עצמו: החלפת PostToolUse לכלי מובנה היא מחרוזת, שנקראת כפי שנכתבה, או פלט כלי כפי ש-grok מסדר אותו, האובייקט המתויג ב-toolResult של אותו אירוע, כך שאובייקט שנכתב מול שמות שדות של סביבת ריצה אחרת לא מפוענח ומתעלמים ממנו. בכלי MCP אין צורה לאכוף, ולכן updatedToolOutput עובר כמו הכינוי updatedMCPToolOutput. ראו פלט PostToolUse.
  • ירי בסוף סשן: ירי Stop נוסף לצפייה בלבד מתרחש בסוף הסשן. סננו לפי reason == "end_turn" (ראו למעלה).
  • תזמוני מרווח: sessionCrons[].schedule הוא מרווח קריא לאדם, אף פעם לא ביטוי cron.
  • סוגי משימות: backgroundTasks[].type הוא רק shell, monitor או subagent. התוויות האחרות של Claude (workflow, teammate, …) לא נפלטות.
  • מחלקות StopFailure: grok פולט שש (rate_limit, authentication_failed, invalid_request, server_error, max_output_tokens, unknown). שגיאות קיבולת (503/529) מסווגות כ-rate_limit. matcher על מחלקת שגיאה ש-grok לא פולט אף פעם לא נורה.
  • ברירת מחדל ל-timeout: grok קובע ברירת מחדל של 5 שניות להוקי צפייה, וזה קצר מרוב המקרים. הגדירו timeout במפורש על הוק מיובא שעושה עבודה אמיתית. /import-claude כותב timeout שבור שמעוגל כלפי מעלה לשניות שלמות (1.5 הופך ל-2). בניות Grok ששוחררו לפני timeouts שבורים קוראות רק שניות שלמות.
  • יציאה 2 עם תשובת JSON על הוק stop: Claude Code חוסם ביציאה 2 בלי קשר למה ש-decision אומר. סיבת decision: "block" מחליפה את stderr כמשוב. Grok קורא קודם את התשובה. תשובה קריאה מחליטה, גם כזו שלא קובעת decision. הוק שמדפיס {"systemMessage": "..."} ויוצא ב-2 נותן לתור להסתיים בלי להחזיר את stderr כמשוב.
  • UserPromptSubmit חוסם, עם פער אחד: יציאה 2 ו-decision: "block" דוחים את הפרומפט כמו Claude, ופרומפט חסום אף פעם לא נכנס להיסטוריית השיחה, אבל stdout / additionalContext של הוק שמתיר נזרקים במקום להתווסף כהקשר.
  • StopCancelled ייחודי ל-grok: הגדרה שמשתמשת בו לא ניתנת להעברה לסביבת ריצה בלי הוק פסיקה.
  • idle_prompt נורה בכל סוף תור: grok יורה אותו גם אחרי תור שנקטע או שנכשל בשגיאה, לא רק אחרי תור שהושלם, כי הוא מדווח על מצב ולא על תוצאה. ה-message שלו הוא טקסט תצוגה ויכול להשתנות בין שחרורים, לכן התאימו לפי notificationType.
  • זהות subagent היא subagentType, לא agent_type: grok שם אותה במטען של האירועים שיכולים להיורה בתוך subagent, בהתאמה ל-SubagentStart/SubagentStop שלו, ולא בשדות המשותפים.
  • ערכי permission_mode: grok פולט default, auto, plan או bypassPermissions. ל-acceptEdits/dontAsk של Claude אין מקבילה ב-grok (auto של grok הוא הקרוב ביותר), ולכן בדיקה כמו permission_mode === "acceptEdits" אף פעם לא מתאימה.
  • timeouts של שער לקוח (SDK): שערי Stop/SubagentStop ב-SDK ברירת המחדל שלהם 600 שניות כמו הוקי קובץ. שערי לקוח PreToolUse ברירת המחדל 30 שניות (המסלול החם האינטראקטיבי). אפשר לעקוף כל אחד לפי קבוצת matcher דרך timeoutS, עם תקרה של 600.
  • /goal: לולאת המטרה של grok היא יכולת נפרדת שרצה לפני שער ה-stop. היא לא הוק Stop מסוג פרומפט.

מדיניות "המשך לעבוד" מלאה בסקריפט אחד:

#!/bin/bash
input=$(cat)
# Gate only genuine turn ends, not the session-end observe fire.
if [ "$(echo "$input" | jq -r '.reason')" != "end_turn" ]; then exit 0; fi
if ! bin/verify.sh >/dev/null 2>&1; then
  echo '{"decision": "block", "reason": "verify.sh failed; fix the failures before finishing"}'
fi

נרשם כ-{ "type": "command", "command": "bin/stop-gate.sh", "timeout": 300 } עם timeout שמותאם לשלב האימות. ההוק נורה שוב אחרי כל המשך, והתקרה המובנית מסיימת את התור אחרי 8. בדקו את stopHookActive כדי לוותר מוקדם יותר על משוב שהסוכן בבירור לא יכול לפעול לפיו.

#הוקים פסיביים

לאירועים כמו SessionStart או Notification, נקרא רק systemMessage עליון ב-stdout. פשוט צאו ב-0 בהצלחה. החריגים הם PreToolUse (ראו פלט (הוקים חוסמים)), Stop/SubagentStop (ראו בקרת החלטת Stop), ו-PostToolUse, שה-stdout שלו נקרא אף על פי שהוא לא חוסם דבר (ראו פלט PostToolUse).

במסלול ה-shell, תור אחד בכל סשן מריץ את שרשרת permission_prompt של בקשת הרשאת הכלי ואת שרשרות Notification של idle_prompt ו-task_complete. כל שרשרת מתחילה כשההתראה שלה מועלית, בזמן ששרשרות קודמות עדיין רצות, עד 32 שרשרות בבת אחת בכל סשן. שרשרת שהועלתה מעבר לזה מתחילה כשאחת מהן מסתיימת. בקשות אישור תוכנית וסקירת diff גם מעלות permission_prompt, אבל הן מריצות את השרשרות שלהן בתוך השורה, מחוץ לתור ולהמתנה ליציאה שלו. שרשרת איטית לא מעכבת את השרשרות שהועלו אחריה עד ש-32 רצות. הפקודה או התור הבאים יכולים להתחיל בזמן שהוקי task_complete רצים. ביציאה, לשרשרות של התור יש 1.5 שניות בסך הכל, לפני SessionEnd, ושרשרת שעדיין רצה אחרי זה נעצרת. הוק ראשון איטי מ-1.5 שניות קוטע את שאר השרשרת שלו, אף על פי שה-timeout של כל הוק ברירת המחדל שלו 5 שניות. ההמתנה הזו יכולה להתחיל את SessionEnd עד 1.5 שניות מאוחר יותר.

במסלול ה-daemon, הוקי Notification חולקים מאגר אחד עם שאר הוקי הצפייה של כל סשן ש-grok host-server מריץ. עד 32 שיגורים במאגר הזה רצים בבת אחת ועוד 64 ממתינים למקום. שיגור שהועלה מעבר ל-64 האלה לא מריץ אף אחד מההוקים שלו, והערה מציינת את ההוקים שהוא דילג עליהם.

#משתני סביבה

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

#משתנים שהרץ מזריק (זמינים תמיד)

המשתנים האלה נקבעים על ידי רץ ההוק עבור כל הוק:

משתנהתיאור
GROK_HOOK_EVENTשם האירוע שהפעיל את ההוק (למשל pre_tool_use, session_start, post_tool_use, session_end, stop, notification).
GROK_HOOK_NAMEהשם המוגדר של ההוק הספציפי הזה (כולל קידומת התוסף להוקים שמספק תוסף).
GROK_SESSION_IDהמזהה הייחודי של סשן Grok הנוכחי.
GROK_WORKSPACE_ROOTנתיב מוחלט לשורש סביבת העבודה הנוכחית. הוא שומר את האיות של cwd של האירוע, עם symlinks שלא נפתרו ובלי לוכסן בסוף.
CLAUDE_PROJECT_DIRשורש סביבת העבודה, התיקייה שבה כל הוק רץ, עם symlinks שנפתרו בזמן שההוקים נטענו. GROK_WORKSPACE_ROOT ו-cwd של האירוע שומרים את איות ההפעלה. זה לא עוקב אחרי קישור שמשתנה אחרי טעינת ההוקים. מוגדר לכל הוק.

המשתנים האלה שמורים. כל ערכים שתנסו לקבוע להם דרך השדה env ב-JSON של ההוק מוסרים בזמן הטעינה (נרשמת אזהרה), והרץ תמיד מזריק את הערכים האמיתיים בזמן ה-spawn.

#משתני הוק של תוסף

כשהוק מקורו בתוסף, Grok מזריק בנוסף את המשתנים הבאים:

משתנהתיאור
GROK_PLUGIN_ROOTנתיב מוחלט לתיקייה המותקנת של התוסף.
GROK_PLUGIN_DATAנתיב מוחלט לתיקיית הנתונים הניתנת לכתיבה של התוסף (לאחסון מצב תוסף, מטמונים וכו').

הערכים האלה מסופקים על ידי מערכת התוספים. לארבעת המפתחות הקשורים לתוסף (GROK_PLUGIN_ROOT, GROK_PLUGIN_DATA וכינויי Claude שלהם), מתאם התוסף מבטיח שהערכים הרשמיים של התוסף תמיד גוברים על כל ערך שהמשתמש הכריז במפת env של ההוק.

#משתני סביבה שהמשתמש מגדיר

אפשר לספק משתני סביבה נוספים למטפל הוק יחיד באמצעות השדה env:

{
  "type": "command",
  "command": "bin/my-hook.sh",
  "env": {
    "MY_SECRET": "value",
    "LOG_LEVEL": "debug"
  }
}

המשתנים האלה מועברים לתהליך ההוק, אבל הם לא יכולים לעקוף את משתני הרץ או התוסף השמורים שרשומים למעלה.

#שימוש במשתנים בשדות command ו-url

גם command וגם url תומכים בהרחבה של ${VAR} ושל $VAR. ב-Windows PowerShell, הפניות $VAR מוכרות נכתבות מחדש ל-$env:VAR כדי שיקראו את סביבת התהליך הבן. ראו את מדריך custom-hooks להרחבה בזמן טעינה מול זמן ריצה, סדר החיפוש במפת env, ומשני הרחבת פרמטרים (למשל ${VAR:-default}).

ב-url, הפניה למשתנה סביבה של Grok שלא מוגדר או ריק נשארת כפי שנכתבה. אם המשתנה עדיין ריק כשההוק רץ, Grok לא שולח את ההוק, כי חלק host ריק היה שולח את הבקשה למארח ששמו מופיע בנתיב. שגיאת הריצה של ההוק מציינת את המשתנה.

#הוקי HTTP

במקום סקריפט מקומי, קראו לנקודת קצה מרוחקת:

{ "type": "http", "url": "https://hooks.example.com/grok-event", "timeout": 15 }

מעטפת האירוע המלאה נשלחת ב-POST כ-JSON.

ה-URL חייב להשתמש ב-https://, או ב-http:// פשוט למארח loopback (127.0.0.0/8, ::1 או localhost). שירות אישור מקומי יכול להאזין ב-http://127.0.0.1:<port>. תחת כל אחת מהסכמות Grok מסרב לאותם טווחים לא ציבוריים כמו web_fetch, מלבד loopback: 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 240.0.0.0/4, ::, fc00::/7, fe80::/10, ff00::/8, וצורות IPv4-mapped של טווחי ה-IPv4. בהגדרת פרוקסי fake-ip (Clash או Surge במצב fake-ip), שם מארח מפוענח אל 198.18.0.0/15. כל בקשה אליו נדחית. זה נשמר גם כש-HTTPS_PROXY מוגדר. Grok עדיין מפענח את המארח בעצמו לפני הבקשה. הוק של מדיניות מנוהלת שם נכשל פתוח בכל קריאה.

Grok בודק את ה-URL כשהוא טוען את ההוק. URL שנדחה הוא שגיאת טעינת הוק לאותו מטפל, ומדווח כמו השגיאות תחת שדות מטפל לא נתמכים. שאר המטפלים בקובץ עדיין נטענים. מטפל של מדיניות מנוהלת עם URL שנדחה מדווח באותו אופן ועדיין נטען. הבדיקה לפני כל בקשה דוחה אותו. במסלול ה-shell, URL שנדחה בהוקי תוסף או הגדרת סוכן רק נרשם בלוג. כשלפחות אחד מההוקים המוטמעים של הסוכן הראשי נטען, מסלול ה-shell גם רק רושם בלוג שגיאות טעינת הוק אחרות, עד שההוקים נטענים מחדש. ב-Linux, תחת פרופיל שה-restrict_network האפקטיבי שלו הוא true (read-only, strict, או פרופיל מותאם שמגדיר או יורש אותו), Grok מסרב ל-http:// פשוט מחוץ למדיניות מנוהלת. הפרופיל מרחיק פקודות הוק מהרשת. בקשת הוק HTTP יוצאת מתהליך Grok עצמו. הוק של מדיניות מנוהלת שומר http:// פשוט למארח loopback. דחייתו הייתה גורמת להוק להיכשל פתוח בכל קריאה. כשמשהו לפני הנתיב עדיין מציין משתנה שהטעינה לא יכולה לפענח, הבדיקה ממתינה לבקשה. לפני כל בקשה Grok מרחיב את ה-URL, בודק אותו שוב, ומפענח את שם המארח. אף כתובת לא יכולה להיות בטווח שנדחה. עבור http:// פשוט, כל כתובת חייבת להיות loopback. בקשה שנדחית שם היא שגיאת הוק. ההוק נכשל פתוח. בלי פרוקסי, הבקשה מתחברת רק לכתובות ש-Grok בדק. פרוקסי מפענח מארח מרוחק בעצמו. בקשה למארח loopback, תחת כל אחת מהסכמות, הולכת ישר אליו ואף פעם לא דרך HTTP_PROXY, HTTPS_PROXY או ALL_PROXY. Grok לא עוקב אחרי הפניות.

#ניהול הוקים ב-TUI

#לשונית ההוקים

הריצו /hooks כדי לפתוח את חלון Extensions בלשונית Hooks (/plugins פותח אותו בלשונית Plugins). בלשונית Hooks:

מקשפעולה
rטעינה מחדש של כל ההוקים מהדיסק
aהוספת הוק מותאם לפי נתיב
xהסרת מקור ההוק הנבחר (מבקש אישור. לחצו y באות קטנה כדי לאשר)
Spaceהפעלה או השבתה של ההוק הנבחר
fמחזור מסנן הסטטוס (All / Enabled / Disabled)

הוקים מקובצים לפי מקור: Global, Project, Plugin ו-Custom.

כל הוק מציג:

  • Event שעליו הוא מופעל
  • Command או URL שרץ
  • משך Timeout
  • Status: מופעל או [disabled]

#פקודות סלאש

/hooks-list           # Show hooks loaded in this session
/hooks-trust          # Trust this project for hook execution
/hooks-add <path>     # Add a custom hook file or directory
/hooks-remove <path>  # Remove a custom hook
/hooks-untrust        # Revoke trust for this project

ב-pager של ה-TUI, פקודות /hooks-* הבודדות לא מופיעות ברשימת פקודות הסלאש. חלון /hooks מכסה רישום, הוספה, הסרה, והפעלה או השבתה של הוקים. אמון בפרויקט מנוהל דרך /hooks-trust (או פעולת Trust של החלון), שכותבת את מאגר אמון התיקיות המאוחד שתואר למעלה.

#הפעלה והשבתה לכל הוק

הפעילו או השביתו הוק יחיד בזמן ריצה בלחיצה על Space בלשונית Hooks. השינוי נכנס לתוקף מיד, בלי להפעיל מחדש את הסשן. השבתת עותק אחד של הוק משביתה גם את העותקים שלו שמריצים את אותו מטפל תחת אותו matcher לאותו אירוע ומגיעים ממקור בסמכות נמוכה יותר, או בסמכות שווה שנטען אחריו. הוקים גלובליים נטענים לפני הוקי פרויקט: השבתת הוק גלובלי משביתה את עותקי הפרויקט שלו, בעוד השבתת עותק פרויקט משאירה את העותקים הגלובליים, של הקונפיג ושל המדיניות המנוהלת רצים. הפעלת עותק שהשבתה של עותק אחר מכסה מפעילה את שניהם, והתוצאה מציינת את העותק האחר. כשהוק נשאר מושבת, Enable אומר למה, להוק אחד או לכל הוק של מקור. עותק תחת matcher אחר שומר את המצב שלו, ועותק של מדיניות מנוהלת עדיין רץ.

הוקים מהגדרת סוכן נקראים על שם הסוכן כולו, כ-agent:<name>:<event>[i].hooks[j]. סוכנים שהשמות שלהם מכילים . או / קיבלו שמות קצרים יותר בעבר (הוקים של agent:review.strict נקראו agent:review:…). השבתה שנשמרה תחת השם הישן כבר לא חלה: השביתו את ההוקים האלה שוב פעם אחת.

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

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

#טעינה מחדש באמצע הסשן

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

#הוקים בשורת הסטטוס ובגלילה לאחור

הוקים שקטים אלא אם הם מעכבים את התור או משנים את מהלכו:

  • כשהתור חסום על אצוות הוקים (שער PreToolUse לפני כלי, שער UserPromptSubmit, שער Stop), שורת הסטטוס מציגה Running pre_tool_use hook… (או Running 3 stop hooks…) אחרי שהאצווה רצה כ-300 מילישניות. הטיימר נספר מרגע שהאצווה התחילה, ולכן הוק איטי מציג את כל ההמתנה, והוק מהיר לא מוצג בכלל.
  • הוק שרץ ואישר לא משאיר עקבות. ה-stdout שלו לא מוצג.
  • הוק שדוחה קריאת כלי, חוסם הנחיה, או עוצר או ממשיך את הסוכן מקבל שורת הערה אחת עם הסיבה. הוקים מ-~/.grok, מקבצי פרויקט ומקבצי תוסף מקבלים שם. הוקים מתצורה מנוהלת נקראים "a managed policy hook".
  • הוק שנכשל (יציאה שאינה אפס, פסק זמן, קריסה, פלט לא תקין) מקבל שורה אחת: <event> hook (<name>) failed, ignored: <reason>, כאשר הסיבה היא קוד היציאה עם שורת ה-stderr הראשונה, או פסק הזמן. "Ignored" הוא מילולי: כשלים הם fail-open, ולכן קריאת הכלי או התור ממשיכים כאילו ההוק אישר.

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

השורות האלה מופיעות רק כשממשק התוספים מופעל (ברירת המחדל).


#דוגמה: שומר מעטפת בטוח

חסימת פקודות מעטפת מסוכנות:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "bin/safe-shell.sh", "timeout": 5 }
        ]
      }
    ]
  }
}

כאשר bin/safe-shell.sh:

#!/bin/sh
INPUT=$(cat)
CMD=$(printf '%s' "$INPUT" | jq -r '.toolInput.command // empty')

if ! printf '%s' "$INPUT" | jq -e '.toolInputTruncated == false' > /dev/null; then
  echo '{"decision": "deny", "reason": "Command too long to check"}'
  exit 2
fi

# Block destructive patterns
if printf '%s\n' "$CMD" | grep -qE '(rm -rf /|mkfs|dd if=|:(){ :|& };:)'; then
  echo '{"decision": "deny", "reason": "Blocked potentially destructive command"}' 
  exit 2
fi

echo '{"decision": "allow"}'

#הערות אבטחה

  • הוקים גלובליים (~/.grok/hooks/) רצים עם הרשאות המשתמש שלך. התייחס אליהם כמו אל סקריפטי מעטפת.
  • הוקי פרויקט דורשים אמון בתיקייה (/hooks-trust או --trust, אותו שער כמו MCP/LSP מקומיים למאגר) כדי למנוע מתקפות שרשרת אספקה ממאגרים זדוניים.
  • הוקי HTTP שולחים נתוני סשן. השתמש רק בנקודות קצה מהימנות.
  • הוק PostToolUse מחליט מה המודל קורא עבור קריאת הכלי הזו. הוא יכול להוסיף הוראות או להחליף את הפלט לגמרי, ולכן יש לתת בו אמון כמו בשער PreToolUse. הגלילה לאחור והתמליל שומרים את הפלט האמיתי, ולכן החלפה תמיד גלויה לך.

#שיטות עבודה מומלצות

  1. שמור על הוקים מהירים: הוקים ארוכים חוסמים את הממשק. השתמש בתהליכי רקע (&) או ב-async כשאפשר.
  2. השתמש ב-deny מפורש כדי לחסום: הוקים נכשלים במצב פתוח בשגיאה. הוק שקורס לא יחסום את הכלי. החריגים הם תשובה מעבר למגבלת קריאה ו-updatedInput שאי אפשר להחיל (ראה איך הוק נפתר). כדי לאכוף מדיניות, ההוק חייב לרוץ עד סיום ולפלוט {"decision":"deny","reason":"..."} ב-stdout. תמיד טפל בשגיאות בתוך הסקריפט כדי שיוכל להחזיר החלטה מפורשת.
  3. השתמש בנתיבים מוחלטים או יחסיים לקובץ ההוק: סקריפטים ב-bin/ ליד קובץ ה-JSON ניידים.
  4. בדוק עם המודאל: הרץ /hooks כדי לוודא שההוקים נטענו ומתאימים לפני שמסתמכים עליהם.
  5. נהל גרסאות של הוקי פרויקט: בצע commit ל-.grok/hooks/ (אבל לעולם לא לסודות).

#פתרון תקלות

  • ההוק לא רץ? הרץ /hooks כדי לראות אם הוא נטען והותאם.
  • הוקים מתעלמים? ייתכן שהתיקייה לא מהימנה. הרץ /hooks-trust (או הפעל מחדש עם --trust).
  • הסקריפט לא נמצא? בדוק שהנתיב יחסי לקובץ ה-.json, או לשורש סביבת העבודה בקובץ הגדרות של Claude, ושהוא בר הרצה (chmod +x). שם חשוף כמו my-guard, בלי קובץ בשם זה ליד קובץ ה-.json, מחופש ב-PATH.
  • רואים שגיאות? לכוד יומנים על ידי הפעלה עם RUST_LOG=debug GROK_LOG_FILE=/tmp/grok.log grok, ואז בדוק את /tmp/grok.log.