الـ hooks هي scripts بتشتغل تلقائيًا لما أحداث معينة تحصل خلال جلسة Claude Code. بتستقبل JSON input عبر stdin وبتتواصل عن طريق exit codes وJSON output. الـ command hooks deterministic، قابلة للاختبار، ومش مرتبطة بلغة معين. أما الـ prompt hooks والـ agent hooks فبيستخدموا Claude model للevaluation، فسلوكهم غير deterministic. في الموديول ده، هنغطي نظام الـ hooks، الأحداث الأساسية، وإزاي تكتب hooks مفيدة.
هيكلة الـ Hooks وإعدادها
الـ hooks بتتحدد في ملفات الإعدادات تحت مفتاح hooks. كل حدث (event) عنده مصفوفة من الـ matchers، وكل matcher عنده مصفوفة من تعريفات الـ hooks. الـ matcher field هو regex pattern بيتطبّق على اسم الأداة — "Bash" بيطابق بالظبط، "Write|Edit" بيطابق أي واحدة فيهم، "*" بيطابق كل الأدوات، "mcp__github__.*" بيطابق كل أدوات GitHub MCP.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.py\"",
"timeout": 10
}
]
}
]
}
}
الـ matchers كمان بتدعم خاصية if المشروطة (v2.1.85) اللي بتستخدم صيغة permission rules عشان تفلتر أكتر إمتى الـ hook يشتغل. الـ matcher بيختار الأداة بالاسم، لكن if بيحدد استدعاءات معينة من الأداة دي. ده مفيد لما تبقى عايز تعترض أوامر git push بس من غير ما تشغّل الـ hook على كل أمر Bash:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"if": "Bash(git push*)",
"hooks": [
{
"type": "command",
"command": "/path/to/check-push.sh"
}
]
}
]
}
}
الـ if pattern بيتبع نفس صيغة permission rules — Bash(git *) بيطابق أي أمر git، وWrite(src/**/test_*.py) بيطابق كتابة ملفات الاختبار، وهكذا. الـ hook اللي عنده if مبيشتغلش غير لما الـ matcher والـ if الاتنين يتطابقوا.
Claude Code بيدعم أكتر من 30 hook event. أكتر الأحداث فائدة في الشغل اليومي: PreToolUse (تحقق قبل ما الأداة تشتغل، ممكن يمنع)، PostToolUse (راقب أو تفاعل بعد ما تخلص، ممكن يضيف سياق)، UserPromptSubmit (اعترض input المستخدم قبل ما Claude يعالجه)، وStop (شغّل فحوصات لما Claude يخلّص رد). فيه كمان أحداث للتعامل مع الصلاحيات (PermissionRequest)، والإشعارات، ودورة حياة الـ subagent (SubagentStart، SubagentStop)، والأخطاء (PostToolUseFailure، StopFailure)، وتغييرات الإعدادات، ومراقبة الملفات (FileChanged)، وضغط السياق (PreCompact، PostCompact)، وإدارة الـ worktree.
كمان فيه أحداث أحدث بتوسّع ردود فعل الـ hooks. CwdChanged (v2.1.83) بيشتغل لما الـ working directory يتغير — زي direnv مثلًا، ممكن تحمّل environment variables تلقائيًا لما Claude يدخل مجلد مشروع. TaskCreated (v2.1.84) بيشتغل لما الأداة TaskCreate تتستخدم، عشان تقدر تسجّل أو تتحقق من المهام الجديدة. WorktreeCreate (v2.1.84) بيشتغل لما worktree agent يتعمل، وبيدعم type: "http" للإشعارات البعيدة. Elicitation (v2.1.76) بيشتغل لما MCP server يطلب input منظم من المستخدم في نص المهمة عبر dialog تفاعلي، وممكن يعترض ويعدّل الـ elicitation قبل ما يتعرض على المستخدم. ElicitationResult (v2.1.76) بيشتغل بعد ما المستخدم يرد على MCP elicitation، وممكن يعترض ويغيّر الرد قبل ما يترجع للـ MCP server.
PreCompact بيشتغل قبل ما Claude Code يضغط المحادثة لتوفير context، ويقدر يمنع الضغط من إنه يحصل — مفيد لما تحب تاخد snapshot للحالة، أو تحذّر المستخدم، أو ترفض auto-compaction ممكن يرمي context مهم. الـ matcher بتاع الحدث بيفرّق بين السبب: "manual" لما المستخدم شغّل /compact، و"auto" لما Claude Code ضغط تلقائيًا لأن الـ context اتملى. امنع الضغط بـ exit code 2 أو بـ JSON فيه قرار:
{
"hooks": {
"PreCompact": [
{
"matcher": "auto",
"hooks": [
{ "type": "command", "command": "./scripts/snapshot-context.sh" }
]
}
]
}
}
رجّع {"decision": "block", "reason": "active refactor in flight"} من السكريبت عشان تسيب المحادثة زي ما هي. PostCompact بيشتغل بعد ما الضغط ينجح، وده المكان المناسب عشان تعيد إرفاق notes، أو تعيد استدعاء skill، أو تسجّل اللي اتحفظ.
سكريبتات الـ hook بتستقبل JSON عبر stdin وعندها وصول لعدة متغيرات بيئة Claude Code بيضبطها تلقائيًا. CLAUDE_CODE_SESSION_ID فيه معرّف الجلسة الفريد — استخدمه لربط سجلات الـ hook والـ telemetry الخارجية بجلسة معيّنة.
hook بـ Python بيقرأ الـ JSON كده:
import json, sys, os
data = json.load(sys.stdin)
tool_name = data.get("tool_name", "")
tool_input = data.get("tool_input", {})
session_id = os.environ.get("CLAUDE_CODE_SESSION_ID", "")
Exit code 0 معناه نجاح (اقرأ JSON stdout للناتج). Exit code 2 معناه خطأ مانع — Claude بيوقف ويعرض رسالة الـ stderr بتاعتك. أي exit code تاني هو تحذير غير مانع بيظهر في verbose mode بس.
الـ input بتاع الـ hook بيحتوي على object effort.level}.level} مع مستوى الجهد الحالي: { "effort": { "level": "medium" } }. المستويات المتاحة هي low وmedium وhigh وmax وauto. نفس القيمة متاحة كمان كمتغير بيئة $CLAUDE_EFFORT في scripts الـ hook، وأوامر الـ Bash اللي بتشغّلها الـ hooks تقدر تقرأها:
import json, os, sys
data = json.load(sys.stdin)
effort_level = data.get("effort", {}).get("level", "medium") # من الـ JSON
effort_env = os.environ.get("CLAUDE_EFFORT", "medium") # من متغير البيئة
أنواع الـ Hooks الشائعة والأنماط
الـ command hooks بتدعم شكلين. Shell form (الافتراضي) بيمرّر الـ command string لـ shell عشان يعمل tokenization. Exec form بيحدد مصفوفة args جنب الـ command، وبيشغّل العملية مباشرة من غير shell — ده بيتجنّب مشاكل الـ shell escaping وأكتر أمانًا لأوامر فيها arguments من المستخدم:
{
"type": "command",
"command": "node",
"args": ["./scripts/validate.js", "--strict"]
}
الـ hooks بتشتغل بخمس طرق. hooks من نوع command بتنفّذ أوامر shell محلية. hooks من نوع prompt بتطلب من Claude يقيّم prompt معين، عادةً على أحداث Stop أو SubagentStop. hooks من نوع agent بتشغّل subagent لعمل تحقق متعدد الخطوات. hooks من نوع http بتعمل POST لنفس الـ JSON payload على webhook endpoint — مفيدة للـ logging البعيد أو خدمات السياسات. الـ HTTP hooks بتدعم interpolation لـ environment variables في الـ headers، والـ variables دي لازم تكون في الـ allowlist صراحةً. hooks من نوع mcp_tool بتستدعي أداة MCP بشكل مباشر — مفيدة لما hook محتاج يتواصل مع خِدْمَات خارجية (زي النشر على Slack أو إنشاء issue على GitHub) بدون استخدام الـ shell. ملاحظة: منشئ الإعدادات في التطبيق لسه ما بيدعمش hooks من نوع mcp_tool — استخدم مثال الـ JSON التالي كمرجع:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "mcp_tool",
"server": "slack",
"tool": "send_message",
"input": { "channel": "#deploys", "text": "Claude finished the task" }
}
]
}
]
}
}
inputs الـ PostToolUse وPostToolUseFailure بتتضمّن خاصية duration_ms فيها وقت تنفيذ الأداة بالميلي ثانية (من غير وقت طلبات الصلاحيات وhooks الـ PreToolUse). استخدمها عشان تتتبّع الأدوات البطيئة أو تعمل تنبيهات لما أداة تتجاوز حد معين.
inputs الـ Stop وSubagentStop كمان بقت تحمل arrays اسمهم background_tasks وsession_crons. الـ background_tasks بيعرض أوامر الـ bash والـ subagents اللي لسه شغّالين في الخلفية وقت ما الدور انتهى؛ والـ session_crons بيعرض المهام المجدولة المرتبطة بالجلسة (/schedule ومراقبات الـ plugins وأي حاجة في الطابور عبر أدوات الـ cron). hook completion-gate ممكن يستخدمهم عشان يخلّي Claude يكمّل لحد ما كل حاجة فعلًا تخلص:
import json, sys
data = json.load(sys.stdin)
pending_bg = [t for t in data.get("background_tasks", []) if t.get("status") in ("running", "starting")]
pending_cron = data.get("session_crons", [])
if pending_bg or pending_cron:
print(json.dumps({
"decision": "block",
"reason": f"{len(pending_bg)} background task(s) and {len(pending_cron)} scheduled task(s) still active"
}))
sys.exit(0)
background_tasks كمان مفيد في SubagentStop عشان parent agent ما يعلنش إنه خلص وفيه bash watchers لسه شغّالين منه.
أنماط شائعة للـ Hooks
تنسيق الملفات تلقائيًا بعد الحفظ من أكتر الـ hooks فائدة. hook من نوع PostToolUse على Write|Edit بيشغّل الـ formatter بتاعك تلقائيًا، عشان output بتاع Claude يبقى دايمًا نظيف:
#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('file_path',''))")
case "$FILE" in
*.ts|*.tsx|*.js) prettier --write "$FILE" 2>/dev/null ;;
*.py) black "$FILE" 2>/dev/null ;;
*.go) gofmt -w "$FILE" 2>/dev/null ;;
esac
exit 0
فحص الأمان على الكتابة بيستخدم PostToolUse مع additionalContext output عشان ينبّه Claude لو كتب secrets محتملة:
SECRET_PATTERNS = [
(r"api[_-]?key\s*=\s*['\"][^'\"]+['\"]", "Potential hardcoded API key"),
(r"password\s*=\s*['\"][^'\"]+['\"]", "Potential hardcoded password"),
]
# ... check content, then:
output = {"hookSpecificOutput": {"hookEventName": "PostToolUse",
"additionalContext": f"Security warnings: {'; '.join(warnings)}"}}
print(json.dumps(output))
hooks الـ PostToolUse كمان ممكن تستبدل output الأداة بالكامل عن طريق updatedToolOutput. Claude بيشوف المحتوى المستبدل بدل الأصلي. ده بيشتغل لكل الأدوات (مش بس MCP) من v2.1.121:
import json, sys
data = json.load(sys.stdin)
original = data.get("tool_result", "")
sanitized = original.replace("/home/user", "~")
output = {"hookSpecificOutput": {"updatedToolOutput": sanitized}}
print(json.dumps(output))
منع الأوامر الخطيرة بيستخدم PreToolUse مع regex check و exit code 2:
BLOCKED = [(r"\brm\s+-rf\s+/", "Blocking dangerous rm -rf /")]
for pattern, message in BLOCKED:
if re.search(pattern, command):
print(message, file=sys.stderr)
sys.exit(2)
متقدم: الـ Prompt Hooks ونطاق المكونات
لأحداث Stop وSubagentStop، نوع الـ hook "prompt" بيستخدم LLM عشان يقيّم اكتمال المهمة. الـ LLM بيقرأ المحادثة ويرجّع قرار منظم — يسيب Claude يوقف ولا يكمّل شغل. ده قوي جدًا للمهام اللي عندها معايير اكتمال واضحة:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Check: 1) Were all files modified? 2) Do tests pass? 3) Is the PR description updated? If anything is missing, explain what.",
"timeout": 30
}
]
}
]
}
}
نوع الـ hook "agent" بيشغّل subagent يعمل التقييم — على عكس الـ prompt hooks (دور واحد)، الـ agent hooks ممكن تستخدم أدوات وتعمل تحليل متعدد الخطوات. استعمل ده لما الفحص محتاج قراءة ملفات أو تشغيل أوامر.
الـ hooks كمان ممكن تتحدد لـ skills وagents معينة باستخدام الـ hooks frontmatter field. hook من نوع PreToolUse في frontmatter الـ skill مبيشتغلش غير وقت تنفيذ الـ skill دي:
---
name: production-deploy
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "./scripts/production-safety-check.sh"
once: true
---
الـ flag once: true بيشغّل الـ hook مرة واحدة بس في كل session بدل ما يشتغل كل مرة الأداة تتطابق. ده مفيد لفحوصات الإعداد اللي محتاجة تحصل مرة واحدة بس.