الوحدة 9 — Hooks: أتمتة سلوك Claude وتثبيته
بيّنت الوحدة 7 كيف نرفض إجراءً بقاعدة صلاحية، وبيّنت الوحدة 8 كيف نتراجع عن خطوة زائدة عبر /rewind. يبقى نقص واحد: السلوك الذي نريده بشكل مضمون، لا رهنًا بمزاج النموذج. تهيئة كلّ ملفّ يُحرَّر، ورفض أيّ كتابة في .env، وإعادة تشغيل الاختبارات قبل تسليم اليد: هذه ليست توصيات، بل قوانين. الـhook هو الآليّة التي تجعلها حتميّة.
الـhook ليس توصية
سطر في CLAUDE.md من قبيل «شغّل دائمًا ruff بعد كلّ تعديل» يقترح سلوكًا على النموذج؛ سيطبّقه غالبًا. أمّا الـhook فهو أمر shell أو نقطة API عبر HTTP أو أداة MCP أو prompt يُنفّذه Claude Code بنفسه في لحظة محدّدة من دورة الحياة. حين يتحقّق الحدث ينطلق الـhook: لا خيار للنموذج. هذا الانتقال من الاحتماليّ إلى الحتميّ هو ما يجعل الـhooks الأداة المفضّلة للفرق التي تحرص على قواعدها الداخليّة دون معركة عند كلّ prompt.
القائمة الدقيقة للأحداث
توثّق الوثائق الرسميّة الأحداث التي يعرّفها Claude Code لصالح الـhooks.
| الحدث | متى يتحقّق |
|---|---|
SessionStart / SessionEnd | جلسة تبدأ أو تُستأنف أو تنتهي |
Setup | --init-only أو -p --init في CI |
UserPromptSubmit / UserPromptExpansion | prompt أُرسل، قبل المعالجة أو التوسيع |
PreToolUse / PostToolUse / PostToolUseFailure | حول استدعاء أداة |
PostToolBatch | بعد دفعة استدعاءات متوازية |
PermissionRequest / PermissionDenied | صلاحيّة مطلوبة أو مرفوضة |
Notification / MessageDisplay | تنبيه، عرض نصّ |
SubagentStart / SubagentStop | ولادة سب-agent وانتهاؤه |
TaskCreated / TaskCompleted | دورة مهمّة |
Stop / StopFailure | نهاية الدور، ربّما بخطأ |
TeammateIdle | زميل خامل في فريق agents |
InstructionsLoaded / ConfigChange | تغيّر في CLAUDE.md أو قاعدة أو إعداد |
CwdChanged / DirectoryAdded / FileChanged | المجلّد الجاري، أو /add-dir، أو ملفّ مُراقب |
WorktreeCreate / WorktreeRemove | إنشاء worktree أو حذفه |
PreCompact / PostCompact | ضغط السياق |
PreModelSwitch / PostModelSwitch | تغيير النموذج |
Elicitation / ElicitationResult | خادم MCP يطلب مُدخلًا |
يقترن حدث أداة بـ matcher يقوم بالتصفية: "Bash"، أو "Edit|Write"، أو "mcp__github__.*". الـmatcher المؤلَّف حصريًّا من حروف وأرقام و_ و- ومسافات و, و| يُعامَل كمطابقة تامّة؛ أيّ محرف آخر يحوّله إلى تعبير نمطيّ JavaScript غير مثبَّت (Edit.* يلتقط Edit وNotebookEdit؛ اكتب ^Edit$ للتثبيت).
ثمّة مرشِّح ثانٍ أدقّ: حقل if في كلّ handler، الذي يقبل نحو صياغة قواعد الصلاحيّات نفسه. if: "Bash(rm *)" لا يُطلَق إلّا إذا طابق الأمر الفرعيّ؛ وif: "Edit(*.ts)" يستهدف TypeScript وحده.
صيغة الدخل والخرج
يتلقّى الـhook من نوع command كائن JSON على stdin ويُبلِّغ نتيجته عبر رمز الخروج، وقد يُعزَّز ذلك بـ JSON على stdout. الحقول المشتركة هي session_id وprompt_id وtranscript_path وcwd وpermission_mode وhook_event_name. ويستقبل حدث PreToolUse أيضًا tool_name وtool_input وtool_use_id. تصل المسارات بفواصل النظام (backslash تحت Windows).
رموز الخروج:
0بلا stdout: نجاح، لا قرار. يستمرّ التدفّق الاعتياديّ.0مع كائن JSON على stdout: يقرأ Claude حقول القرار. المسار الموصى به.2: خطأ حاجب في الأحداث التي يمكن حجبها (PreToolUseوUserPromptSubmitوStopوSubagentStopوTaskCreatedوTaskCompletedوConfigChangeوPreCompactوPreModelSwitchوPostToolBatchوElicitationوWorktreeCreate). محتوى stderr يُستخدم رسالةً.- رمز آخر: خطأ غير حاجب؛ يستمرّ الإجراء وتظهر ملاحظة في السجلّ.
يقبل JSON الصادر على stdout طبقتَين. الحقول العامّة هي continue (ضبطه على false يوقف Claude بالكامل) وstopReason وsystemMessage وterminalSequence. أمّا حقل hookSpecificOutput فينقل القرارات الخاصّة بكلّ حدث.
| الأحداث | حقول القرار |
|---|---|
UserPromptSubmit، PostToolUse، PostToolBatch، Stop، SubagentStop، ConfigChange، PreCompact | decision: "block" في الجذر، مع reason |
PreToolUse | hookSpecificOutput.permissionDecision (allow / deny / ask / defer)، permissionDecisionReason، updatedInput، additionalContext |
PermissionRequest | hookSpecificOutput.decision.behavior (allow / deny)، updatedInput، message |
PermissionDenied | hookSpecificOutput.retry: true |
SessionStart، SubagentStart | سياق عبر hookSpecificOutput.additionalContext |
Elicitation / ElicitationResult | hookSpecificOutput.action (accept / decline / cancel)، content |
أحداث تتبّع (Notification، SessionEnd، PostCompact، CwdChanged، FileChanged…) | آثار جانبيّة، لا قرار |
ثلاثة أحداث تسمح بـإعادة كتابة المحتوى في الطيران: PreToolUse.updatedInput (الوسائط قبل التنفيذ)، وPermissionRequest.decision.updatedInput (جانب الـprompt)، وPostToolUse.updatedToolOutput (النتيجة المرئيّة لـClaude، والأداة قد نفّذت فعلًا).
الأنواع الأربعة لـhooks
يختار حقل type في كلّ handler المنفِّذ.
command— shell. يستقبل JSON على stdin، ويردّ برمز خروج وstdout. الحقول:command،args(exec form بلا shell)،async،shell(bashأوpowershell).http— نقطة API. الحقول:url،headers(توسيع$VARمقصور علىallowedEnvVars)،timeout. يُرسل Claude كائن JSON بطريقة POST.mcp_tool— أداة على خادم MCP متّصل. الحقول:server،tool،input(استبدال${tool_input.file_path}).prompt— يتحوّل JSON إلى$ARGUMENTSداخل prompt يُقيَّم بنموذج Claude، فيردّ بـ JSON قرار.
نوع خامس، agent، مُصنَّف تجريبيًّا.
أين نُصرِّح hook
تعيش الـhooks في عدّة أماكن وتُجمَع بين الطبقات: hook مشروع لا يستبدل hook مستخدم.
| الموقع | النطاق |
|---|---|
~/.claude/settings.json | جميع مشاريعك |
.claude/settings.json | مشروع واحد، تحت الإدارة الإصداريّة |
.claude/settings.local.json | مشروع واحد، لك وحدك |
| Managed policy settings | المنظّمة كلّها |
Plugin: hooks/hooks.json | حين يكون الـplugin مُفعَّلًا |
Frontmatter لـSKILL.md أو سب-agent | الجلسة، بمجرّد الاستدعاء |
يفتح الأمر /hooks متصفّحًا للقراءة فقط يعرض جميع الـhooks المُهيَّأة وحدثها ومطابقها ومصدرها (User Settings، Project Settings، Local Settings، Plugin Hooks، Session Hooks).
هناك placeholder-ان لكتابة مسارات محمولة: ${CLAUDE_PROJECT_DIR} (جذر المشروع، مكشوف أيضًا متغيّرَ بيئة) و${CLAUDE_PLUGIN_ROOT} (داخل plugin، موقع التثبيت).
Kiosque: ثلاثة hooks تُنجز العمل
يُبرمج فريق Kiosque مرّةً واحدةً ما كان يذكِّر به يدويًّا: تهيئة كلّ ملفّ Python يُحرَّر، ورفض أيّ كتابة في migrations/ و.env، وعدم تسليم اليد ما دامت الاختبارات حمراء.
Hook 1 — تنسيق تلقائيّ بعد كلّ تعديل
يوضَع في .claude/hooks/format-python.sh ويُجعَل قابلًا للتنفيذ. يقرأ هذا السكربت JSON PostToolUse من stdin، ويستخرج المسار، ولا يتفاعل إلّا مع ملفّات .py، ثمّ ينفّذ ruff format يليه ruff check --fix. يخرج بـ 0: أثر جانبيّ، لا قرار.
#!/usr/bin/env bash
# .claude/hooks/format-python.sh
INPUT=$(cat)
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty')
FILE_PATH="${FILE_PATH//\\//}"
if [[ -z "$FILE_PATH" || "$FILE_PATH" != *.py ]]; then
exit 0
fi
ruff format "$FILE_PATH" >/dev/null 2>&1
ruff check --fix "$FILE_PATH" >/dev/null 2>&1
exit 0
Hook 2 — منع الكتابة في migrations/ و.env
PreToolUse بمطابق Edit|Write يرفض أيّ مسار حسّاس. القاعدة مضاعَفة في permissions.deny من الوحدة 7، لكنّ الـhook يؤكّد لحظة الإجراء ويقدّم سببًا لـClaude، فيتمكّن من اقتراح بديل.
#!/usr/bin/env bash
# .claude/hooks/protect-paths.sh
INPUT=$(cat)
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty')
FILE_PATH="${FILE_PATH//\\//}"
case "$FILE_PATH" in
*/migrations/*|*/.env|*.env.local)
jq -n --arg p "$FILE_PATH" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: ("الكتابة ممنوعة في " + $p + ". استخدم migration من Alembic أو مدير أسرار.")
}
}'
exit 0
;;
esac
exit 0
يُعيد الـhook قرار deny مُنسَّقًا: يستقبل Claude السبب ويتفاعل — اقتراح migration من Alembic بدلًا من الإصرار.
Hook 3 — لا تسليم لليد إلّا إذا نجحت make test
يحجب hook Stop نهاية الدور إذا فش لت الاختبارات. فخّ كلاسيكيّ: بلا حارس، يُطلَق في كلّ استئناف ويدور بلا نهاية. نُضيف حارسًا صريحًا عبر stop_hook_active.
#!/usr/bin/env bash
# .claude/hooks/require-green-tests.sh
INPUT=$(cat)
ACTIVE=$(printf '%s' "$INPUT" | jq -r '.stop_hook_active // false')
if [[ "$ACTIVE" == "true" ]]; then
exit 0
fi
if make test >/tmp/kiosque-test.log 2>&1; then
exit 0
fi
TAIL=$(tail -n 20 /tmp/kiosque-test.log | jq -Rs .)
jq -n --argjson tail "$TAIL" '{
decision: "block",
reason: ("المجموعة `make test` تفشل. مقتطف:\n" + $tail)
}'
الحقل decision: "block" في الجذر هو عقد خرج Stop: يستأنف Claude اليد مع السبب، ويصحّح، ويعيد التشغيل. في الاستئناف الثاني، تكون قيمة stop_hook_active هي true فيتنحّى الـhook.