يمكن لـ hook أن يوقف تعديلًا قبل أن يغيّر ملفًا. صحيح أنها ليست طريقة مثالية لحماية الملفات، لكنها تمنع الأخطاء أحيانًا، ومن المفيد تطبيقها في بعض الحالات.
إذا أردنا رفض عملية كتابة معيّنة، فعلينا فحص المُدخَلات المقترحة للأداة قبل تنفيذها. والـ hook من نوع PreToolUse يُشغَّل عندها بالضبط، قبل أن يتغيّر الملف. الفحص بعد الكتابة يستطيع أن يخبرنا بما حدث، لكن عندها يكون قد فات الأوان لمنع ذلك الاستدعاء.
لنفترض أن المشرف على المشروع يدير أرقام الإصدارات بشكل منفصل. نريد أن يغيّر الوكيل كود التطبيق، وأن يترك config/release.json للمشرف. نضيف إلى الإعدادات hook من نوع PreToolUse، والـ matcher فيه «Edit أو Write». قبل تشغيل أيٍّ من الأداتين، يقرأ الـ handler مسار الملف المقترح ويقارنه بالمسار المحمي.
وحّدوا صيغة المسار قبل مقارنته، لأن صيغًا مختلفة قد تشير إلى الملف نفسه. فالمسار النسبي، والمسار الذي يحتوي على «..»، والمسار المطلق، كلها تؤدي إلى الملف نفسه في المشروع. يحدّد الـ handler عندنا المسار الفعلي، ويتحقّق من أنه داخل المشروع. والمسار الذي لا يستطيع تفسيره، أو الذي يقع خارج المشروع، يُرفض مع سبب خاص به. أما ملف الكود counts.py فمساره الفعلي مختلف، لذلك يتركه هذا الـ hook لفحوصات الصلاحيات العادية.
عند تعديل الملف المحمي، يطبع الـ handler هذا الـ JSON على الإخراج القياسي. يقرأ Claude Code ردّ PreToolUse، ويرفض استدعاء الأداة لأن قيمة permissionDecision هي deny. ويبقى الملف دون تغيير.
ويخبر السببُ الوكيلَ بالملف الذي لا يجوز له تعديله، ويطلب منه أن يتواصل مع المشرف بدلًا من ذلك. ولأي ملف آخر، لا يطبع الـ handler شيئًا، فيتابع Claude Code فحوصات الصلاحيات العادية. وقرار allow صريح سيوافق على الاستدعاء عبر هذا الـ hook، لذلك نترك القرار دون تحديد حين لا تنطبق قاعدة هذا الملف.
في هذا التشغيل المسجَّل، حاول الوكيل تغيير رقم الإصدار من 1.4.0 إلى 1.5.0. فرفض Claude Code التعديل، وأعاد سبب الـ hook إلى الوكيل. بقي الإصدار القديم في الملف، وأخبر الوكيل المستخدم بأنه لم يُجرِ التغيير.
الـ matcher عندنا يغطي Edit وWrite. لكن أمر Bash يستطيع أن يكتب في الملف نفسه دون أن يمرّ بهذا الـ handler، مثلًا بإعادة توجيه المُخرَجات أو بتشغيل سكربت. والبحث في الأمر عن كلمة مريبة لا يكشف لنا كل ما سيفعله ذلك السكربت.
هذا الـ hook مفيد لإبعاد التعديلات العادية عن ملف إعدادات الإصدار. وإذا كان يجب منع العملية من الكتابة في هذا الملف بأي أداة، فافرضوا هذا القيد بصلاحيات نظام الملفات أو ببيئة معزولة.
حتى الآن، تحقّقنا مما إذا كان التعديل المقترح مسموحًا به. والآن لننظر في استخدام آخر للـ hooks. حين ينتهي الوكيل من ردّه، يستطيع hook من نوع Stop أن يفحص المشروع ويطلب من الوكيل مواصلة العمل. هذا لا يتراجع عن التعديلات السابقة. لكنه يعطي الوكيل سببًا للمتابعة، بدلًا من أن ينهي ردّه وهناك فحصٌ نعرف أنه يفشل.
الـ hook من نوع Stop في مثالنا يتحقّق مما إذا كان المشروع يجتاز فحوصاته حين يحاول الوكيل إنهاء ردّه. يشغّل الـ handler الأمر bash tools/check.sh على الملفات الحالية، مع مهلة زمنية مدتها دقيقتان. يستخدم هذا المثال سكربت الفحص الموجود في المشروع. ويمكن للـ hook أيضًا أن يطلب حكمًا من نموذج. لكن هذا يعني طلبًا إضافيًا إلى النموذج، وقد يختلف ردّه، لذا استخدموا برنامجًا حين يستطيع أن يقرّر.
السكربت قصير. يشغّل الاختبارات، وأداة فحص الأنواع، والـ linter، وفحص التنسيق، بهذا الترتيب. في هذا السكربت التسلسلي البسيط، يوقف set -e التنفيذ حين يفشل أحد الأوامر، ويصبح ذلك الفشل نتيجةَ السكربت. وإذا نجحت كل الأوامر، نجح السكربت.
إذا نجح الفحص، لا يطبع الـ handler شيئًا ولا يمنع الإنهاء. وإذا فشل الفحص، يعيد الـ handler قرار block مع سبب يطلب مزيدًا من العمل. ينسخ الـ handler عندنا أسطر الفشل من مُخرَجات الاختبارات إلى ذلك السبب، وهنا، الـ assertion الذي يقول إن 0 لا يساوي 5. لاحظوا أن هذا الردّ يستخدم صيغة مختلفة عن صيغة قرار الصلاحيات في PreToolUse. استخدموا صيغة الحدث الذي تعالجونه.
ماذا يحدث إذا لم يستطع الوكيل إصلاح الفشل؟ في هذا المثال، يسمح الـ hook بمحاولة تصحيح واحدة، ثم يفحص مرة أخرى. وإذا فشل الفحص الثاني أيضًا، يُنهي إعادة المحاولة التلقائية ويُبلِغ المستخدم بالمشكلة التي لم تُحلّ. وانتهاء المهلة الزمنية أو تعطّل بيئة الفحص يحتاج أيضًا إلى تدخّل إنسان، لذلك يتحوّل هذا النوع من الفشل مباشرةً إلى تحذير.
كيف يعرف الـ handler أن هذه إعادة محاولة؟ يرسل Claude Code معلومات الحدث بصيغة JSON إلى الإدخال القياسي للـ handler. يُظهر المقتطف على اليسار الحقل stop_hook_active وقيمته true. يضبط Claude Code هذا الحقل حين يواصل الوكيل العمل بعد أن منع hook من نوع Stop إنهاء الردّ. يقرأ الـ handler هذا الحقل، ولا يحتاج إلى عدّاد محاولات خاص به في هذا المثال. ومع ذلك، يشغّل الـ handler فحص المشروع مجددًا. فإذا فشل وقيمة هذا الحقل true، يحذّر الـ handler المستخدم بدلًا من أن يعيد قرار block آخر. وبهذا نحدّ من إعادة المحاولة التلقائية.
عند تكرار الفشل، يطبع الـ handler هذا الـ JSON، وفيه systemMessage. يعرض Claude Code هذا التحذير للمستخدم، وينتهي ردّ الوكيل. يقول التحذير إن الفحوصات ما زالت تفشل، ويشير إلى ملف السجلّ المحفوظ. وانتهاء الردّ لا يعني أن العمل اجتاز الفحوصات.
رأيتم كيف يستطيع hook أن يرفض استدعاء Edit أو Write على ملف محمي، أو أن يطلب من الوكيل المتابعة بعد فشل أحد الفحوصات. كم سيكون لطيفًا لو كان على الثلاجة hook من نوع PreToolUse يعمل في منتصف الليل. فيرفض الطلب، والسبب: «أكلتم الحلوى قبل قليل». نراكم في الفيديو التالي.