انتقل إلى المحتوى الرئيسي

الوحدة 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 / UserPromptExpansionprompt أُرسل، قبل المعالجة أو التوسيع
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، PreCompactdecision: "block" في الجذر، مع reason
PreToolUsehookSpecificOutput.permissionDecision (allow / deny / ask / deferpermissionDecisionReason، updatedInput، additionalContext
PermissionRequesthookSpecificOutput.decision.behavior (allow / denyupdatedInput، message
PermissionDeniedhookSpecificOutput.retry: true
SessionStart، SubagentStartسياق عبر hookSpecificOutput.additionalContext
Elicitation / ElicitationResulthookSpecificOutput.action (accept / decline / cancelcontent
أحداث تتبّع (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 مقصور على allowedEnvVarstimeout. يُرسل 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.

ملفّ .claude/settings.json الناتج

تُجمَع الـhooks الثلاثة في ملفّ إعدادات مشروع واحد، تحت الإدارة الإصداريّة مع Kiosque. يضمن ${CLAUDE_PROJECT_DIR} إيجاد السكربتات انطلاقًا من الجذر.

{
"hooks": {
"PostToolUse": [
{ "matcher": "Edit|Write", "hooks": [
{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format-python.sh" }
]}
],
"PreToolUse": [
{ "matcher": "Edit|Write", "hooks": [
{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/protect-paths.sh" }
]}
],
"Stop": [
{ "hooks": [
{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/require-green-tests.sh", "timeout": 300 }
]}
]
}
}

تُودِع نادية الملفّ في git؛ يستعيد كريم وليا الـhooks عند git pull المقبل. لا حاجة إلى أيّ توصية في CLAUDE.md لتذكير Claude بالتنسيق: يتمّ ذلك دونه.

التصحيح والمزالق التي يجب تفاديها

تتكرّر ثلاثة أخطاء. السكربت لا ينطلق: سكربت غير قابل للتنفيذ يفشل بالرمز 127 (السجلّ: Failed with non-blocking status code)؛ أودِع في git بت +x. تحت Windows، يفشل exec form على shims الـ.cmd: استعمل shell: powershell. JSON الخرج بلا أثر: profile shell يطبع عند البدء يلوّث stdout ويكسر التحليل؛ يجب ألّا يحوي stdout سوى كائن JSON. claude --debug يفيد سجلًّا. hook يدور بلا نهاية: Stop سيّئ الحراسة يُنتج دورات؛ stop_hook_active هو الحارس، ويقطع Claude Code بعد ثمانية حجوب متتالية.

hook بدل skill

PreToolUse على Bash(git commit *) قادر على رفض commit من دون أن يفكّر أحد في كتابة skill. اترك الـskills لسير العمل حيث يقرّر الإنسان؛ واحفظ الـhooks لضوابط الأمان حيث لا حاجة له في القرار.

الأمان: تعمل الـhooks بامتيازات الجلسة، بلا terminal مسيطر على macOS وLinux. لا تنفّذ أبدًا داخل hook شيفرةً غير مُدقَّقة قادمة من plugin أو مصدر خارجيّ.

الخلاصة

  • الـhook حتميّ: حين يتحقّق الحدث ينطلق الأمر، بمعزل عن قرار النموذج.
  • تغطّي الأحداث دورة الحياة كاملةً: جلسة، ودور، وأداة، وتنبيه، وضغط، وتغيير نموذج.
  • يصل الدخل بصيغة JSON على stdin؛ ويمرّ الخرج عبر رمز الخروج (0، 2، آخر) وعبر كائن JSON على stdout يحوي decision أو reason أو hookSpecificOutput.
  • أربعة أنواع مستقرّة: أمر، وHTTP، وأداة MCP، وprompt مُقيَّم بنموذج؛ ونوع خامس، agent، تجريبيّ.
  • تُصرَّح الـhooks في الإعدادات (user، وproject، وlocal)، وفي plugin، أو في frontmatter لـskill أو سب-agent، وتتراكم بين الطبقات.
  • لـKiosque، ثلاثة hooks تكفي: تنسيق تلقائيّ، ومسارات محميّة، واختبارات خضراء قبل نهاية الدور.

الوحدة التالية: السب-agents، والوكلاء المتوازون، وفرق agents — كيف نُفوِّض عملًا طويلًا لوكيل معزول، ونُشغِّل خمس تحقيقات متوازية، ونُدير فريقًا كاملًا من المحادثة الرئيسة.