6.4 הוקים הרצאה
עד עכשיו בפרק הזה הרחבנו את יכולות הagent - חיברנו tools חיצוניים דרך MCP, והאצלנו משימות לsub-agents. אבל בכל אלה, ההחלטה מה לעשות נשארה בידי המודל. לפעמים אנחנו רוצים משהו שונה: התנהגות דטרמיניסטית שתקרה תמיד, בלי תלות בשיקול הדעת של המודל. למשל, "הרץ את המעצב אחרי כל עריכת קובץ", או "חסום כל פקודה שמנסה למחוק קבצים". בשביל זה יש הוקים - hooks. הוק הוא סקריפט שמופעל אוטומטית באירוע מסוים במחזור החיים של הagent. בהרצאה הזו נבין אילו אירועים קיימים, מה מבנה ההגדרה, ואיך כותבים הוק אמיתי ששומר על איכות הקוד.
למה הוקים - the motivation¶
ההבדל המהותי בין הוק לבין הוראה ב-CLAUDE.md הוא מי מבצע. כשכותבים ב-CLAUDE.md "תמיד הרץ את הבדיקות אחרי שינוי", זו בקשה מהמודל - והמודל עלול לשכוח או להחליט אחרת. הוק, לעומת זאת, מבוצע על ידי המערכת (ה-harness), לא על ידי Claude. הוא ירוץ תמיד, בלי קשר למצב הרוח של המודל.
זו נקודה קריטית לכל מי שרוצה אוטומציה אמינה: אם משהו חייב לקרות בכל פעם, הוא צריך להיות הוק, לא הוראה. הוקים הם הדרך להפוך העדפה רכה למנגנון קשיח.
אירועי ההוק - hook events¶
הוק נקשר לאירוע במחזור החיים. הנה האירועים המרכזיים שכדאי להכיר:
| אירוע - event | מתי הוא נורה |
|---|---|
| PreToolUse | לפני שtool מורץ (אפשר לחסום) |
| PostToolUse | אחרי שtool הצליח |
| UserPromptSubmit | כשהמשתמש שולח הודעה |
| Stop | כש-Claude מסיים לענות |
| SubagentStop | כשsub-agent מסיים |
| SessionStart | כשסשן מתחיל או מתחדש |
| SessionEnd | כשסשן מסתיים |
| Notification | כש-Claude Code שולח התראה |
| PreCompact | לפני דחיסת context |
שני האירועים הכי שימושיים בפרקטיקה הם PreToolUse ו-PostToolUse. הראשון מאפשר לבדוק ולחסום פעולה לפני שהיא קורית (למשל למנוע פקודה מסוכנת). השני מאפשר להגיב אחרי שפעולה קרתה (למשל להריץ מעצב אחרי עריכה).
מבנה ההגדרה - settings.json¶
הוקים מוגדרים בקובץ settings.json. יש כמה מיקומים לפי הטווח:
~/.claude/settings.json - all projects, local to you
.claude/settings.json - a single project, goes into git and is shared
.claude/settings.local.json - a single project, local, does not go into git
מבנה ההגדרה בנוי משלוש רמות: האירוע, ה-matcher (איזה tool או תבנית תפעיל את ההוק), ומערך ה-hooks עם הפקודות עצמן. הנה דוגמה מלאה שחוסמת פקודות מחיקה:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh"
}
]
}
]
}
}
נפרק את המבנה:
PreToolUse: האירוע. מערך, כי אפשר כמה כללים לאותו אירוע.matcher: כאן"Bash"- ההוק ירוץ רק לפני tool Bash. עבור עריכות היינו כותבים"Edit|Write"(ביטוי רגולרי).type: "command": סוג ההוק. הנפוץ הוא פקודת מעטפת.command: הסקריפט שירוץ. המשתנה${CLAUDE_PROJECT_DIR}מצביע על שורש הפרויקט, כדי שהנתיב יעבוד תמיד.
מה ההוק מקבל - stdin input¶
כשההוק רץ, Claude Code מעביר לו JSON דרך stdin. זה מאפשר לסקריפט לדעת בדיוק מה קורה. הנה דוגמה למה שהוק PreToolUse מקבל לפני פקודת Bash:
{
"session_id": "abc123",
"cwd": "/Users/me/project",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}
הסקריפט קורא את ה-JSON, שולף את מה שמעניין אותו (למשל את הפקודה), ומחליט מה לעשות. בדרך כלל משתמשים ב-jq כדי לשלוף שדות.
שליטה בהתנהגות - exit codes¶
איך ההוק משפיע על מה שקורה? הדרך הפשוטה היא קוד היציאה של הסקריפט:
- קוד 0: הצלחה. הכל ממשיך כרגיל. ב-PreToolUse הtool ירוץ.
- קוד 2: שגיאה חוסמת. המשמעות תלויה באירוע:
- ב-
PreToolUse: הtool נחסם, וה-stderr מוחזר ל-Claude. - ב-
UserPromptSubmit: ההודעה נדחית. - ב-
Stop: מונע מ-Claude לעצור, וממשיך את השיחה. - קוד אחר: שגיאה לא חוסמת. הביצוע ממשיך, וההערה נרשמת בלוג.
בנוסף לקודי היציאה, הוק יכול להחזיר JSON מובנה ב-stdout (בקוד 0) לשליטה מדויקת יותר, כולל החלטת הרשאה. אבל להרבה מקרים, קוד היציאה מספיק.
דוגמה אמיתית: חסימת מחיקה - block-rm¶
בואו נכתוב את הסקריפט שההגדרה למעלה הפנתה אליו, .claude/hooks/block-rm.sh. הוא חוסם פקודות rm -rf:
#!/bin/bash
# .claude/hooks/block-rm.sh
COMMAND=$(jq -r '.tool_input.command' < /dev/stdin)
if echo "$COMMAND" | grep -q 'rm -rf'; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "the delete command was blocked by a hook"
}
}'
else
exit 0
fi
הסקריפט קורא את הפקודה מה-JSON, ואם היא מכילה rm -rf, הוא מחזיר JSON שמורה למערכת לדחות את הפעולה עם סיבה. אחרת, הוא יוצא בקוד 0 והפעולה ממשיכה כרגיל. שימו לב: זו חסימה דטרמיניסטית - היא לא תלויה בשיקול הדעת של המודל.
דוגמה אמיתית: מעצב אחרי עריכה - formatter¶
הדוגמה השימושית ביותר ליומיום היא הרצת מעצב או linter אחרי כל עריכת קובץ. זה מבטיח שהקוד תמיד מעוצב, בלי שהמודל יצטרך לזכור. ההגדרה:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/run-formatter.sh"
}
]
}
]
}
}
ה-matcher כאן "Edit|Write" - ההוק ירוץ אחרי כל עריכה או כתיבה של קובץ. הסקריפט יכול לשלוף את נתיב הקובץ מה-JSON ולהריץ עליו את המעצב המתאים:
#!/bin/bash
# .claude/hooks/run-formatter.sh
FILE=$(jq -r '.tool_input.file_path // empty' < /dev/stdin)
# run the formatter based on the extension
case "$FILE" in
*.py) black "$FILE" ;;
*.js|*.ts) prettier --write "$FILE" ;;
*.go) gofmt -w "$FILE" ;;
esac
exit 0
עכשיו, בכל פעם ש-Claude עורך קובץ Python, black ירוץ עליו אוטומטית. אין דרך שהמודל "ישכח" לעצב.
תבניות ה-matcher - matcher patterns¶
ה-matcher קובע מתי בדיוק ההוק נורה, והוא גמיש. הערך שלו הוא ביטוי רגולרי שמושווה לשם הtool:
"Bash" - only the Bash tool
"Edit|Write" - edit or write (the pipe | means "or")
"Edit|Write|MultiEdit" - all three change tools
"" - an empty matcher catches everything
עבור אירועים שאין להם tool (כמו UserPromptSubmit או SessionStart), ה-matcher לרוב מיותר או מקבל ערך כמו "startup". נקודה חשובה: אפשר לתפוס גם tools של MCP. אם רוצים הוק שרץ לפני tool מסוים משרת MCP, ה-matcher יהיה השם המלא בתבנית mcp__<server>__<tool> שראינו בהרצאה 6.2. למשל, "mcp__db__.*" יתפוס כל tool מהשרת db.
סוגי הוקים - hook types¶
עד כה ראינו type: "command" - פקודת מעטפת. זה הסוג הנפוץ, אבל יש סוגים נוספים שכדאי לדעת שקיימים:
| סוג - type | מה הוא עושה |
|---|---|
| command | מריץ סקריפט מעטפת (הנפוץ ביותר) |
| http | שולח בקשת HTTP לשרת חיצוני |
| prompt | שואל מודל קטן שאלה (למשל "האם הפקודה בטוחה?") |
| agent | מפעיל agent לאימות פעולה |
עבור רוב הצרכים, command מספיק ונותן שליטה מלאה. הסוג prompt מעניין: הוא מאפשר להכניס שיקול דעת של מודל לתוך ההוק, למשל לבדוק אם פקודה נראית מסוכנת. אבל שימו לב - זה כבר לא דטרמיניסטי לחלוטין, אז לחסימות קשיחות עדיף command עם לוגיקה מפורשת.
שילוב עם בדיקות - the Stop hook¶
טריק חזק במיוחד הוא שימוש באירוע Stop. אפשר להגדיר הוק שכש-Claude מנסה לסיים, מריץ את חבילת הבדיקות, ואם היא נכשלת - חוסם את העצירה ומחזיר את השגיאות ל-Claude כדי שיתקן. כך הagent לא "מכריז על ניצחון" בזמן שהבדיקות אדומות.
הנה ההגדרה:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/run-tests.sh"
}
]
}
]
}
}
והסקריפט מריץ בדיקות, ואם הן נכשלות, יוצא בקוד 2 עם הודעה ל-stderr - מה שמונע את העצירה וגורם ל-Claude להמשיך ולתקן:
#!/bin/bash
# .claude/hooks/run-tests.sh
if ! npm test > /tmp/test-output.txt 2>&1; then
echo "Tests failed. Fix the errors before finishing:" >&2
tail -20 /tmp/test-output.txt >&2
exit 2
fi
exit 0
זה מדגים את העיקרון: הוקים הם דרך לאכוף משמעת שאנחנו לא רוצים לסמוך עליה שהמודל יזכור לבד. במקום לקוות שהagent יריץ בדיקות, המערכת מכריחה אותו.
פלט JSON מובנה - structured output¶
מעבר לקודי יציאה, הוק יכול להחזיר JSON ב-stdout (בקוד 0) לשליטה מדויקת. ראינו את זה בדוגמת חסימת המחיקה. הנה השדות השימושיים:
{
"continue": true,
"systemMessage": "a warning to display",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "the reason for the block"
}
}
השדה permissionDecision יכול להיות deny (חסום), allow (אשר בלי לשאול), או ask (שאל את המשתמש). זה חזק יותר מקוד יציאה כי הוא מאפשר גם לאשר פעולה אוטומטית, לא רק לחסום. כך אפשר לבנות מדיניות הרשאות מותאמת - למשל, לאשר אוטומטית פקודות git קריאה בלבד, ולחסום push לענף ראשי.
הוקים בsub-agents ובskills - scoped hooks¶
עד כה ראינו הוקים גלובליים ב-settings.json, אבל אפשר לצמצם הוק לsub-agent או לskill ספציפיים. בכותרת ה-YAML של sub-agent אפשר להגדיר הוקים שירוצו רק כשאותו agent פעיל:
---
name: db-reader
description: runs read-only queries
tools: Bash
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "./scripts/validate-readonly-query.sh"
---
זה חזק בשילוב עם מה שראינו בהרצאה 6.3: sub-agent שהוא קריאה בלבד, עם הוק שחוסם כל פקודת SQL שמנסה לכתוב. ההוק בודק את הפקודה ויוצא בקוד 2 אם הוא מזהה INSERT, UPDATE, DELETE וכדומה. כך אכיפה דטרמיניסטית מוצמדת לagent ספציפי, ומנוקה כשהוא מסיים.
בנוסף, יש אירועים ברמת הסשן שמגיבים למחזור החיים של sub-agents: SubagentStart (כשagent מתחיל) ו-SubagentStop (כשagent מסיים). אפשר להשתמש בהם למשל כדי להכין חיבור לפני שagent מסד נתונים מתחיל, ולנקות אחריו.
שיקולי זהירות - caution¶
הוקים מריצים קוד שרירותי במחשב שלכם, אוטומטית. לכן:
- בדקו סקריפטים לפני שאתם סומכים עליהם, במיוחד כאלה שמגיעים מ-
.claude/settings.jsonבריפו של מישהו אחר. - שמרו על הוקים מהירים. הוק PostToolUse רץ אחרי כל עריכה; אם הוא איטי, הוא יאט את כל העבודה.
- היו זהירים עם קוד 2 ב-PreToolUse. חסימה רחבה מדי תתסכל את הagent ותמנע ממנו לעבוד.
סיכום¶
בהרצאה הזו למדנו:
- הוק - hook הוא סקריפט שמופעל אוטומטית באירוע במחזור החיים. ההבדל המהותי מ-CLAUDE.md: הוק מבוצע על ידי המערכת, לא על ידי המודל, ולכן הוא רץ תמיד ולא תלוי בשיקול דעת.
- האירועים המרכזיים:
PreToolUse(לפני tool, אפשר לחסום),PostToolUse(אחרי tool),UserPromptSubmit,Stop,SubagentStop,SessionStartועוד. - הוקים מוגדרים ב-
settings.json(בטווח~/.claude/,.claude/, או.claude/settings.local.json). המבנה: אירוע,matcher(tool או תבנית), ומערךhooksעםtype: "command"ו-command. - ההוק מקבל JSON דרך stdin עם פרטי האירוע (כמו
tool_nameו-tool_input), ושולף שדות עםjq. - שליטה בהתנהגות דרך קוד יציאה: 0 להצלחה, 2 לחסימה (המשמעות תלויה באירוע - ב-PreToolUse חוסם את הtool), או JSON מובנה ב-stdout להחלטת הרשאה מדויקת.
- דוגמאות מרכזיות: חסימת
rm -rfעם PreToolUse, הרצת מעצב/linter אחרי עריכה עם PostToolUse, והרצת בדיקות ב-Stop כדי למנוע סיום בזמן שהבדיקות אדומות. - זהירות: הוקים מריצים קוד אוטומטית - בדקו סקריפטים ממקור זר, שמרו עליהם מהירים, ואל תחסמו רחב מדי.