تخطَّ إلى المحتوى
NerdLevelTech
الأمان والخصوصية

إخفاقات Claude Code Hook: تم اختبار كتلة onFailure

تعمل خطافات Claude Code بنظام الفشل المفتوح في معظم الأحداث. لقد اختبرت كتلة onFailure عبر أربعة إصدارات مع حالات مثل السكربتات المفقودة، وexit 1، ومهلات الانتظار، وأخطاء المخطط.

Nerd Level Techنُشر في ٢٠ دقيقة للقراءةEnglish
صورة غلاف: إخفاقات Claude Code Hook: تم اختبار كتلة onFailure

أضاف Claude Code الإصدار 2.1.295 خيار onFailure: "block" لخطافات (hooks) الأوامر وHTTP.١ وبدونه، في معظم الأحداث، فإن الخطاف الذي يفشل أو ينتهي وقته أو لا يبدأ أبداً يسمح باستمرار الإجراء.٢

في اختباراتي، قام الإصداران 2.1.295 و 2.1.296 بحظر هذه الإخفاقات. أما الإصداران 2.1.294 و 2.1.287 فقد تجاهلا هذا الإعداد.

ملخص

  • بشكل افتراضي، فإن خطاف السياسة الذي يحتوي على خطأ مطبعي في مساره لا يوقف الإجراء في معظم الأحداث. لقد أعدت إنتاج هذه الحالة في الإصدار 2.1.296 باستخدام سكربت مفقود، ورمز خروج (exit code) بقيمة ١، وانتهاء وقت التنفيذ، مع السماح المسبق بـ Bash حتى لا يظهر طلب الإذن.
  • يقوم "onFailure": "block" بتحويل كل حالة من هذه الحالات إلى استدعاء أداة محظور أو طلب محظور. وفي خطاف UserPromptSubmit، قام أيضاً باكتشاف ملفات JSON التي يتم تحليلها ولكنها تكسر مخطط الخطاف (hook schema)، والنصوص المغلفة بأقواس والتي لا تعد JSON صالحاً، واتصال HTTP مرفوض.
  • الإصداران الأقدم اللذان اختبرتهما، 2.1.294 و 2.1.287، تجاهلا هذا الحقل. وفي حالات Bash، لم تذكر مخرجاتهما هذا الإعداد أبداً، لذا فإن الإعدادات التي تبدو آمنة في تثبيت قديم قد لا تكون كذلك في الواقع.
  • رمز الخروج ٢ لا يزال يحظر الإجراء عن قصد. استخدمه للرفض الفعلي، واترك onFailure للحالات التي يتعطل فيها الخطاف نفسه.
  • تتعامل الوثائق مع مخرجات stdout لخطاف الأوامر غير المتزامن (non-async) على أنها JSON فقط عندما تكون كائناً واحداً يبدأ بـ { وينتهي بـ }. أما الجزء المبتور مثل {"a":1, فيعتبر نصاً عادياً، لذا في UserPromptSubmit، حيث اختبرته، فإنه يمر حتى مع ضبط block إذا خرج السكربت بالقيمة ٠.٢

ما ستتعلمه

  • ما هو onFailure وأي أنواع الخطافات تدعمه
  • كيف اختبرته، وما الذي تم تشغيله على كل إصدار
  • جداول النتائج الكاملة لخطافات UserPromptSubmit وخطافات أدوات Bash
  • ثلاثة سلوكيات تستحق المعرفة، بما في ذلك نقطة عمياء لأجزاء JSON ناتجة عن الوثائق
  • سكربت حماية صغير وملف إعدادات يمكنك نسخه
  • ما الذي لا يغطيه هذا الحقل

ماذا يفعل onFailure

onFailure هو حقل في تعريف الخطاف ضمن إعدادات Claude Code. وهو يقبل القيمة "continue" (الافتراضية)، أو "block"، وتذكره الوثائق لخطافات command و http فقط.٢

جاء في سجل التغييرات changelog الخاص بشركة Anthropic للإصدار 2.1.295، بتاريخ ٨ أكتوبر ٢٠٢٦: "تم إضافة onFailure: "block" لخطافات الأوامر وHTTP: الخطاف الذي لا يمكنه البدء، أو ينتهي وقته، أو يخرج برمز غير متوقع يقوم بحظر الإجراء بدلاً من السماح بمروره".١

المشكلة التي يحلها مذكورة بوضوح في مرجع الخطافات: "في معظم الأحداث، عندما يفشل الخطاف أو ينتهي وقته، يستمر Claude Code في تنفيذ الإجراء، لذا فإن خطاف السياسة بمسار خاطئ أو سكربت يتعطل يسمح بمرور كل شيء".٢

هذا تصميم يعتمد على "الفشل المفتوح" (fail-open). من وجهة نظري، هذا يناسب خطاف التنبيهات، ولكنه غير مناسب لخطاف وظيفته هي قول "لا".

تعتبر الوثائق خمسة أشياء بمثابة فشل: أمر لا يمكن بدؤه، رمز خروج غير ٠ أو ٢، خطأ في اتصال HTTP أو حالة غير 2xx، انتهاء وقت التنفيذ، ومخرجات غير صالحة. أما مخرجات stdout النصية العادية من خطاف الأوامر فهي صراحةً لا تعتبر فشلاً.٢

عندما يتم تعيين block، فإن الفشل يقوم بما يفعله رمز الخروج ٢ عند وقوع هذا الحدث، باستثناء في PermissionRequest، حيث يتم رفض الطلب. يؤدي فشل خطاف (hook) PreToolUse إلى حظر استدعاء الأداة، ويؤدي فشل خطاف UserPromptSubmit إلى حظر المطالبة.٢

لمعرفة كيفية تكامل الخطافات ومطالبات الموافقة في Claude Agent SDK، راجع دليل التدخل البشري (human-in-the-loop) الخاص بنا. وبالنسبة للمكافئ من جانب الإضافة (plugin)، راجع ملاحظة .catch في منشورنا حول حالات استخدام تعديلات Claude Code.

كيف قمت باختبار ذلك

قمت بتثبيت أربعة إصدارات من npm في مجلدات منفصلة: @anthropic-ai/claude-code بالإصدارات 2.1.296 و 2.1.295 و 2.1.294 و 2.1.287. ويقوم كل منها بالإبلاغ عن إصداره باستخدام claude --version.

يظهر السجل أن الإصدار 2.1.287 نُشر في ١ أكتوبر ٢٠٢٦، و 2.1.294 في ٨ أكتوبر، و 2.1.295 في ٨ أكتوبر، و 2.1.296 في ٩ أكتوبر.٣ الإصداران الأقدم يسبقان الإصدار الذي أضاف هذا الحقل، لذا فهما يعملان كمجموعات ضابطة.

وكإجراء للتحقق المتقاطع، يظهر النص blocking because onFailure في الملفات الثنائية للإصدارين 2.1.295 و 2.1.296 ولا يظهر في أي من الإصدارين الأقدم.

تم تشغيل كل حالة باستخدام claude -p مع تمرير ملف إعدادات عبر --settings، على نظام Linux x64 مع Node إصدار v22.22.0. وكان لكل خطاف في المصفوفة مهلة زمنية قدرها ٣ ثوانٍ.

كانت سكربتات الخطاف عبارة عن ملفات Node صغيرة: أحدها يخرج بالرمز ٠، وآخر يخرج بالرمز ١ بعد الكتابة في stderr، وآخر يخرج بالرمز ٢، وآخر ينتظر لمدة ٣٠ ثانية، وآخر يطبع نصاً عادياً، والعديد منها يطبع مخرجات تشبه JSON بأشكال مختلفة. وهناك حالة إضافية تشير إلى مسار سكربت غير موجود، وأخرى تستخدم خطاف HTTP موجه إلى منفذ مغلق.

نوعان من الأحداث، لأنهما يفشلان بطرق مختلفة:

  • UserPromptSubmit. يعمل الخطاف قبل أن يعالج النموذج المطالبة.٢ تعيد المطالبة المحظورة النص UserPromptSubmit operation blocked by hook. أما المطالبة المقبولة فتحصل على رد طبيعي.
  • PreToolUse على Bash. طلبت من النموذج تشغيل touch <marker file> باستخدام --allowedTools Bash، حتى لا تعترض مطالبة الإذن الطريق. ثم تحققت من شيئين: ما إذا كان ملف العلامة موجوداً بعد ذلك، وماذا قالت tool_result في تدفق JSON.

اعتمد التحقق من العلامة على قيام النموذج بنسخ مسار الملف بشكل صحيح. ولهذا السبب تعتبر tool_result هي وسيلة التحقق المتقاطع.

المحاولة الأولى التي أعطى فيها التحقق من العلامة إجابات خاطئة كانت بسبب إعدادات الاختبار الخاصة بي، لذا قمت باستبعادها.

إليك ما تم تشغيله وأين. تم تشغيل المصفوفة الكاملة على الإصدارين 2.1.296 و 2.1.287، باستثناء صفوف Bash الخاصة بقرار السماح والخروج بالرمز ٢، والتي تم تشغيلها على 2.1.296 فقط.

على الإصدارين 2.1.295 و 2.1.294، قمت بتشغيل مجموعات أقصر.

بالنسبة للإصدار 2.1.295، وهو الإصدار الذي أضاف الحقل، كانت هناك ست سلوكيات لـ UserPromptSubmit وأربعة سلوكيات لـ Bash، تشغيل واحد لكل منها، بالإضافة إلى حالة "تعذر البدء" أدناه. وبالنسبة للإصدار 2.1.294، وهو أقرب إصدار قبل التغيير، كانت هناك ثمانية سلوكيات لـ UserPromptSubmit وحالات Bash لسكربت مفقود، والخروج بالرمز ١، ومهلة زمنية.

تم تشغيل كل حالة UserPromptSubmit على 2.1.296 و 2.1.287 مرة واحدة في المصفوفة، باستثناء الحالات ذات شكل JSON في أسفل الجدول الأول، والتي تم تشغيلها من مرتين إلى خمس مرات. وتأتي نتائج Bash من سكربت الاختبار المصحح، الذي قام بتشغيل كل حالة مرتين بنفس النتيجة في المرتين، باستثناء الحالتين المحددتين بتشغيل واحد.

bash
npm install @anthropic-ai/claude-code@2.1.296
claude -p "say hi" --settings ./settings.json --output-format stream-json --verbose --include-hook-events < /dev/null
claude -p "Use the Bash tool to run exactly this command and nothing else: touch /tmp/marker" --settings ./settings.json --allowedTools Bash --output-format stream-json --verbose --include-hook-events < /dev/null

النتائج في لمحة سريعة

توضح الشبكة أدناه كل صف من الجدولين.

اللون الفيروزي يعني أن الـ hook أوقف المطالبة (prompt) أو الأمر، والبرتقالي يعني أن هناك فشلاً قد مر، والكهرماني يعني نصاً عادياً، وهو ما لا تعتبره الوثائق فشلاً، والرمادي يعني أن الحالة نجحت كما ينبغي أو أنني لم أختبرها.

القسمان التاليان يحتويان على نفس البيانات الموجودة في الجداول.

شبكة من نتائج الاختبار. لكل سلوك hook، ما إذا كان Claude Code 2.1.296 و 2.1.287 قد أوقفا مطالبة UserPromptSubmit أو أمر Bash تحت الإعداد الافتراضي وتحت كتلة onFailure. في إصدار 2.1.296 مع block، تم إيقاف السكربت المفقود، و exit 1، و timeout، و JSON صالح ثم exit 1، و JSON غير صالح للمخطط (schema-invalid)، واتصال HTTP مرفوض، و JSON غير صالح محاط بأقواس. في إصدار 2.1.287، أدى exit 2 فقط إلى إيقاف أي شيء.

تم نقل الخلايا من المخرجات المحفوظة لتشغيلات الاختبار الخاصة بي في ١٣ أكتوبر ٢٠٢٦، وتم التحقق منها. الخلايا المحددة بـ "not tested" هي حالات لم أقم بتشغيلها على إصدار 2.1.287. إصدارات 2.1.295 و 2.1.294 قامت بتشغيل مجموعات أقصر وتطابقت مع 2.1.296 و 2.1.287.

النتائج: hooks الخاصة بـ UserPromptSubmit

يعتبر الـ hook من نوع UserPromptSubmit أسرع طريقة لرؤية العمل الميداني، حيث أن المطالبة المحظورة تتوقف قبل أن يجيب النموذج. "Passed" تعني أن النموذج أجاب. "Blocked" تعني أن الـ hook أوقف المطالبة.

سلوك الـ Hook2.1.296 الافتراضي2.1.296 block2.1.287 الافتراضي2.1.287 block
Exits 0PassedPassedPassedPassed
مسار السكربت غير موجودPassedBlockedPassedPassed
Exits 1 مع رسالة stderrPassedBlockedPassedPassed
Exits 2 مع رسالة stderrBlockedBlockedBlockedBlocked
تجاوز مهلة الـ ٣ ثوانٍ (Sleeps)PassedBlockedPassedPassed
يطبع JSON صالح، ثم exits 1PassedBlockedPassedPassed
يطبع JSON يفشل في المخطط، exits 0PassedBlockedPassedPassed
HTTP hook، الاتصال مرفوضPassedBlockedPassedPassed
يطبع نصاً عادياً، exits 0PassedPassedPassedPassed
يطبع {not json}، exits 0PassedBlockedPassedPassed
يطبع {not json بدون قوس إغلاق، exits 0PassedPassedPassedPassed
يطبع نصف كائن، {"a":1,، exits 0PassedPassedPassedPassed

صف "valid JSON, then exit 1" يتطابق مع الوثائق. عند ضبط block، فإن أي رمز خروج (exit code) غير ٠ أو ٢ يعتبر فشلاً حتى لو قام الـ hook بطباعة JSON صالح.٢

جدول Bash أدناه يحتوي على الإصدار الذي يطبع قرار السماح (allow decision).

استخدم صف المخطط (schema) القيمة {"decision":"maybe"} وفي متغير ثانٍ، قيمة additionalContext قدرها 5. كانت نتيجة الـ hook في تدفق الأحداث (event stream) هي error في كلتيهما، وتم حظرهما تحت block.

صف {not json} هو فشل في التحليل (parse failure) وليس فشلاً في المخطط. كما أبلغ عن error وتم حظره.

النتائج: hooks على استدعاءات أدوات Bash

هذه هي الحالة التي تحدد ما إذا كان الأمر سيعمل. "Ran" تعني أن ملف العلامة (marker file) كان موجوداً وكانت نتيجة الأداة ناجحة. "Not run" تعني عدم وجود ملف علامة ونتيجة أداة تحمل خطأ الـ hook.

سلوك الـ Hook2.1.296 الافتراضي2.1.296 block2.1.287 الافتراضي2.1.287 block
خروج بكود ٠تم التشغيلتم التشغيلتم التشغيلتم التشغيل
مسار السكريبت غير موجودتم التشغيللم يتم التشغيلتم التشغيلتم التشغيل
خروج بكود ١تم التشغيللم يتم التشغيلتم التشغيلتم التشغيل
تجاوز مهلة الانتظار (٣ ثوانٍ)تم التشغيللم يتم التشغيلتم التشغيلتم التشغيل
طباعة قرار بالسماح، ثم خروج بكود ١ (تشغيل واحد)تم التشغيللم يتم التشغيللم يتم اختبارهلم يتم اختباره
خروج بكود ٢ (تشغيل واحد)لم يتم التشغيللم يتم التشغيللم يتم اختبارهلم يتم اختباره

عندما يتم الحظر، يكون نص الخطأ محددًا. بالنسبة للسكريبت المفقود، كان النص PreToolUse:Bash hook error: [node .../missing.js]: failed; blocking because onFailure is "block"، متبوعًا بـ stack trace الخاص بـ Node.

بالنسبة للسكريبت الذي يتجاوز المهلة، تم استبدال كلمة failed بكلمة timed out. وهذا هو الصياغة التي تعد بها الوثائق.٢

تستخدم صفوف السكريبت المفقود node مع مسار سكريبت غير موجود، لذا يبدأ Node ثم يخرج بكود ١. وهذا يتطابق مع الاختبار الخاص بالوثائق، والذي يترك السكريبت مفقودًا.٢

تدرج الوثائق السكريبت المفقود أو الملف التنفيذي المفقود كأمثلة على hook لا يمكنه البدء.٢ قمت أيضًا بتشغيل مسار أمر غير موجود، على Bash فقط، تشغيل واحد لكل منهما. عند التشغيل عبر الـ shell، خرج بكود ١٢٧.

بالنسبة لمسار الأمر غير الموجود، لم يتم تشغيل الأمر تحت block في الإصدارين 2.1.296 و 2.1.295، بينما تم تشغيله تحت block في الإصدارين 2.1.294 و 2.1.287. ومع الإعداد الافتراضي في 2.1.296، تم التشغيل.

لقطة شاشة لنص tool_result الذي أعاده Claude Code 2.1.296 لاستدعاء Bash عندما كان سكريبت الحماية مفقودًا وكان onFailure هو block: is_error true، والسطر PreToolUse:Bash hook error متبوعًا بـ failed; blocking because onFailure is "block"، ثم MODULE_NOT_FOUND stack trace الخاص بـ Node.

نص tool_result من تشغيل حقيقي في ١٣ أكتوبر ٢٠٢٦، معروض كصورة من المخرجات الملتقطة. لم يتم إنشاء ملف العلامة (marker file).

أعمدة الإصدارات هي النقطة الأهم. في الإصدار 2.1.295، تطابقت كل حالة قمت بتشغيلها مع 2.1.296.

في حالات 2.1.287 التي قمت بتشغيلها، لم يكن لإعداد block أي تأثير. وفي حالات Bash التي أعدت تشغيلها على كلا الإصدارين الأقدم، لم تذكر المخرجات أبدًا onFailure وكان stderr فارغًا.

في الإصدار 2.1.294، جميع حالات UserPromptSubmit للسكريبت المفقود، والخروج بكود ١، وتجاوز المهلة، وخطأ في المخطط (schema error)، و {not json}، ورفض اتصال HTTP قد مرت مع ضبط block. أما الخروج بكود ٢ فظل محظورًا. وحالات Bash للسكريبت المفقود، والخروج بكود ١، وتجاوز المهلة، تم تشغيلها جميعًا.

إذا كان فريقك يتشارك ملف إعدادات وكان بعض الأشخاص يستخدمون إصدارًا أقدم، مثل الإصدارين اللذين اختبرتهما، فإن الملف سيبدو آمنًا هناك ولكنه ليس كذلك.

ثلاثة سلوكيات تستحق المعرفة

تعتبر أجزاء JSON كنص عادي. تشير مراجع الـ hooks إلى أن stdout الخاص بـ command hook غير متزامن (non-async) يتم تحليله كـ JSON عندما تكون المخرجات بالكامل عبارة عن كائن JSON واحد.٢ المخرجات التي لا تبدأ بـ {، أو تبدأ بـ { ولكن لا تنتهي بـ }، تعتبر نصًا عاديًا.

المخرجات التي تبدأ بـ { وتنتهي بـ } ولكنها ليست JSON صالحًا تعتبر فشلاً في التحليل (parse failure).٢

تضيف الوثائق قاعدة منفصلة لعدة أسطر يتم تحليل كل منها كـ JSON بشكل مستقل، وهو أمر لم أقم باختباره.٢

تتبع نتائجي قاعدة الكائن الواحد في UserPromptSubmit. مع ضبط block على الإصدار 2.1.296، تم حظر {not json} في كلتا محاولتيه. أما {not json فقد نجحت في جميع محاولاتها الخمس، ونجحت {"a":1, في كلتا محاولتيها.

لذا، فإن خطاف (hook) من نوع UserPromptSubmit يطبع جزءاً من قراره ولا يزال يخرج بالقيمة ٠ يتم تمريره. وهذا يتطابق مع القاعدة الموثقة، ولكن من السهل إغفال ذلك. اجعل السكريبت يخرج بقيمة غير صفرية عند كل مسار خطأ، واترك onFailure يتولى الباقي.

المطالبة المحظورة لا تزال تخرج بالقيمة ٠ في عمليات تشغيل claude -p الخاصة بي. عندما قام خطاف UserPromptSubmit بحظر مطالبة، استمر claude -p في الخروج بالقيمة ٠ في فحصي. وقد انطبق ذلك على خطاف يخرج بالقيمة ١ تحت إعداد block وعلى خطاف يخرج بالقيمة ٢.

شهد مدونة المطورين في Classmethod رمز خروج ٠ لمطالبة محظورة أيضاً، مع خطاف يخرج بالقيمة ١، في تقريرهم عن نفس الإصدار.٤ في بيئة CI، اقرأ المخرجات بدلاً من حالة الخروج.

يمكن أن يكون مهلة الانتظار الافتراضية طويلة. تذكر الوثائق أن ٦٠٠ ثانية هي القيمة الافتراضية لخطافات الأوامر، وHTTP، وأدوات MCP، والتي تم خفضها إلى ٣٠ ثانية في UserPromptSubmit وحدثي تبديل النموذج، وإلى ١٠ ثوانٍ في MessageDisplay.٢ لقد قمت بضبطها على ٣ ثوانٍ في المصفوفة و٥ ثوانٍ في اختبار الحماية أدناه.

سكريبت حماية يفشل في حالة الإغلاق (fails closed)

إليك سكريبت حماية PreToolUse صغير. يقوم برفض rm -rf عمداً بالقيمة ٢ ويسمح بمرور كل شيء آخر. إذا كانت المدخلات ليست JSON صالحاً، يقوم السكريبت برمي خطأ ويخرج بالقيمة ١، وتحت إعداد block، فإن الخروج بالقيمة ١ يوقف الاستدعاء، كما توضح الجداول أعلاه.

js
let raw = "";
process.stdin.on("data", (chunk) => (raw += chunk));
process.stdin.on("end", () => {
  const input = JSON.parse(raw);
  const command = (input.tool_input && input.tool_input.command) || "";
  if (/\brm\s+-rf\b/.test(command)) {
    process.stderr.write("Blocked: rm -rf is not allowed here\n");
    process.exit(2);
  }
  process.exit(0);
});

التعبير النمطي (regex) هو مجرد عرض توضيحي. فهو يغفل متغيرات مثل rm -fr و rm -r -f، لذا تعامل معه كنقطة بداية. احفظه باسم .claude/hooks/check-command.js وقم بتوجيه ملف .claude/settings.json الخاص بالمشروع إليه:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "node",
            "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/check-command.js"],
            "timeout": 5,
            "onFailure": "block"
          }
        ]
      }
    ]
  }
}

قمت بتشغيل كلا الملفين على الإصدار 2.1.296 في مجلد git جديد. عند التشغيل مباشرة عبر Node، خرج ls بالقيمة ٠، وخرج rm -rf build بالقيمة ٢ مع الرسالة، وخرج النص not json بالقيمة ١.

من خلال Claude Code، تم تشغيل أمر touch وتم حظر أمر rm -rf برسالة السكريبت الخاصة. وعندما قمت بتغيير اسم السكريبت، تم حظر الاستدعاء بنص failed; blocking because onFailure is "block".

عندما قمت بتغيير "block" إلى "continue" وغيرت اسم السكريبت مرة أخرى، تم تشغيل touch. وهذا هو الوضع الافتراضي للفشل المفتوح (fail-open).

ما لا يغطيه onFailure

تقول الوثائق أن هذا الحقل ليس له تأثير على خطافات Stop و SubagentStop و TaskCompleted و TeammateIdle، لأن رمز الخروج ٢ في هذه الأحداث يعيد Claude إلى العمل، ولا يمكن لـ Claude إصلاح خطاف لا يعمل. كما أنه لا يفعل شيئاً لخطافات الأوامر الخلفية التي تضبط async أو asyncRewake.٢

تدرج جداول الحقول هذا الخيار لخطافات command و http فقط، لذا فإن خطافات المطالبة والوكيل وأدوات MCP لا توثق مثل هذا المفتاح.٢

أنا لم أختبر أيًا من هذه الحالات. كما أنني لم أختبر PostToolUse، حيث تكون الأداة قد عملت بالفعل بحلول وقت فشل الـ hook، ولا PermissionRequest، حيث تذكر الوثائق أن الفشل يرفض الطلب عندما يتم تعيين block.٢

تم اختبار أخطاء الـ Schema، وJSON غير السليم، وHTTP hooks على UserPromptSubmit فقط، وكانت حالة HTTP عبارة عن اتصال مرفوض، وليس استجابة غير 2xx أو رد بطيء.

كود الخروج ١ ونوم لمدة ٣٠ ثانية يمثلان حالات الانهيار (crashes) والتوقف (hangs). لم أختبر عملية تم إنهاؤها قسريًا (killed process).

كل شيء هنا تم تشغيله عبر claude -p مقابل النموذج الافتراضي لـ CLI على منصة واحدة، وقمت بقراءة تدفق أحداث JSON لمعظم الحالات بدلاً من الواجهة التفاعلية. النتائج هي عينة وليست ضمانًا.

قائمة مراجعة للنشر

قراءتنا: تعامل مع هذا كقائمة مراجعة قصيرة بدلاً من إعادة كتابة السياسة.

  1. قم بتعيين "onFailure": "block" في كل أمر PreToolUse أو UserPromptSubmit أو HTTP hook يقوم برفض شيء ما.
  2. احتفظ بكود الخروج ٢ للرفض المتعمد، واترك onFailure يغطي الحالات التي يتعطل فيها الـ hook نفسه.
  3. قم بتعيين timeout يتناسب مع الـ hook. عمل الـ guard الخاص بي تحت مهلة ٥ ثوانٍ بدون مشاكل، وهو أقل بكثير من الافتراضي البالغ ٦٠٠ ثانية.
  4. اترك السكريبت مفقودًا لمرة واحدة، كما تقترح الوثائق، وتأكد من رؤية نص blocking because onFailure.٢
  5. ثبّت إصدارًا أدنى. يتطلب الحقل الإصدار 2.1.295 أو أحدث،٢ وقد تجاهلته النسختان الأقدم اللتان اختبرتهما، دون أي ذكر لـ onFailure في مخرجات Bash الخاصة بهما.
  6. خطط للجانب السلبي. الـ guard المعطل الآن يمنع المكالمات التي يطابقها، لذا قم بنشر تغييرات الـ hook مع اختبار، كما تفعل مع أي بوابة حماية.

لماذا يهم هذا في الممارسة العملية

إليك تقرير من العالم الحقيقي. تقرير عن GitHub issue تم فتحه في ٢٦ يوليو ٢٠٢٦ ضد Claude Code 2.1.220 يذكر وجود ٦٬٨٦٥ إدخال من نوع hook_non_blocking_error في جلسة واحدة، جميعها بكود خروج ١٢٧.٥

يعزو المبلغ ذلك إلى مسار مشروع غير مقتبس يحتوي على مسافة. يقول إن أحد عشر PreToolUse hooks قد تأثرت، وتدرج تفاصيلهم اثني عشر سكريبت، أحدها يسمى verify-on-stop.sh.٥

لقد قاموا بعد الإدخالات في ملف نص الجلسة، ويقول عنوان المشكلة أنه لم تكن هناك إشارة مرئية.٥ هذا تقرير مستخدم واحد، وأنا لم أتمكن من إعادة إنتاج المشكلة.

تظهر نسخة claudeissues.com أن المشكلة أُغلقت كـ "غير مخططة" في ٨ سبتمبر ٢٠٢٦.٥ لم أتحقق من سبب إغلاقها.

إذا كان التقرير دقيقًا، فهو يوضح كيف يبدو الـ fail-open في الممارسة العملية. يتم تكوين الـ guards، وتفشل الـ hooks في التشغيل، والأثر الوحيد الذي وجده المبلغ كان سجلاً في ملف النص.

بالنسبة لنمط حظر الأسرار الذي يحمي هذا، راجع تعديل Claude Code المختبر الذي يحظر قراءة .env. وللمخاطر الأوسع المتعلقة بالإضافات، راجع Plugin4Shell وتحصين وكلاء البرمجة الذكية.

الخلاصة

خطاف السياسة (policy hook) الذي يفشل في وضع "الفتح" (fails open) يتوقف عن فرض القيود في اللحظة التي يتعطل فيها. بدءًا من الإصدار 2.1.295، يغطي إعداد واحد خطافات الأوامر وHTTP، مع الاستثناءات المذكورة أعلاه.

في اختباراتي على الإصدار 2.1.296، استمر العمل في حالة فقدان السكريبت، أو الخروج بكود exit 1، أو انتهاء المهلة (timeout) في كلا نوعي الأحداث. وفي UserPromptSubmit، استمر العمل أيضًا في حالة أخطاء المخطط (schema errors)، أو JSON غير صالح محاط بأقواس، أو رفض اتصال HTTP.

أضف هذا الإعداد إلى كل أمر PreToolUse أو UserPromptSubmit أو خطاف HTTP يقوم بالرفض، وقم بتثبيت الإصدار، واختبر مسار الفشل مرة واحدة.

هناك نقطة عمياء ناتجة عن قاعدة التحليل (parsing rule) الموثقة، وقد ثبت ذلك في اختباري: يتم اعتبار جزء من JSON كنص عادي، لذا اجعل السكريبت يخرج بقيمة غير صفرية في كل مسار خطأ.

المراجع

الأسئلة الشائعة

ماذا يفعل خيار onFailure "block" في Claude Code؟

يجعل هذا الخيار أي أمر فاشل أو HTTP hook يمنع (block) الإجراء بدلاً من السماح له بالاستمرار. يُعتبر الـ hook فاشلاً إذا تعذر تشغيله، أو انتهى وقته (timeout)، أو خرج بكود غير ٠ أو ٢، أو أعاد خطأ HTTP، أو أنتج مخرجات لا يمكن تحليلها أو تكسر المخطط (schema). لا تُعتبر مخرجات stdout النصية البسيطة من command hook فشلاً. القيمة الافتراضية هي "continue".

أي إصدار من Claude Code أضاف onFailure؟

الإصدار 2.1.295، الذي تم إصداره في ٨ أكتوبر ٢٠٢٦. في اختباراتي، التزم الإصدار 2.1.295 بهذا الإعداد، بينما تجاهله الإصداران 2.1.294 و 2.1.287.

هل كود الخروج ١ يمنع (block) الـ hook؟

ليس بشكل افتراضي. تصف الوثائق كود الخروج ١ بدون JSON صالح بأنه خطأ غير مانع (non-blocking error) وتوصي باستخدام الخروج ٢ لـ policy hooks. لقد لاحظت الشيء نفسه في الإصدار 2.1.296: حيث سمح hook بخروج ١ بمرور كل من prompt وأمر Bash حتى أضفت onFailure: "block".

هل يعمل onFailure على hooks الخاصة بـ prompt أو agent أو MCP tool؟

تدرجه جداول الحقول في الوثائق لـ command و HTTP hooks فقط. لم أقم باختبار الأنواع الأخرى.

هل يؤثر onFailure على Stop hooks؟

وفقاً للوثائق، لا. يذكرون أنه ليس له تأثير على hooks من نوع Stop، و SubagentStop، و TaskCompleted، و TeammateIdle، أو على background command hooks. لم أختبر هذه الحالات.

كيف أتحقق من أن الـ hook الخاص بي يفشل في وضع الإغلاق (fails closed)؟

قم بنقل السكريبت أو إعادة تسميته، واطلب من Claude تشغيل أمر Bash غير ضار، وابحث عن عبارة failed; blocking because onFailure is "block" في الخطأ، كما تقترح الوثائق. ثم استعد السكريبت وتأكد من أن الأمر يعمل.

هل تُعتبر مخرجات JSON غير الصالحة فشلاً؟

يعتمد ذلك على شكلها. وفقاً للوثائق، بالنسبة لـ non-async command hook، فإن المخرجات التي تبدأ بـ { وتنتهي بـ } ولكنها ليست JSON صالحاً تُعتبر فشلاً في التحليل (parse failure)، وفي اختباري قام {not json} بالمنع (block) تحت إعداد block. أما المخرجات التي لا تبدأ بـ { وتنتهي بـ } معاً فتُعتبر نصاً عادياً، وقد مرّ كل من {not json و {"a":١, بنجاح. وتضيف الوثائق قاعدة منفصلة لعدة أسطر يتم تحليل كل منها كـ JSON، وهو ما لم أختبره.

هل يغير الـ prompt الممنوع كود الخروج لـ claude -p؟

في فحوصاتي على الإصدار 2.1.296 ظل الكود ٠، لذا يجب على وظيفة CI قراءة نص المخرجات بدلاً من حالة الخروج.

المراجع

  1. Claude Code changelog, Anthropic, version 2.1.295, October 8, 2026. Source for the release date and the changelog wording. ↩ ↩2 ↩3

  2. Hooks reference, Claude Code documentation, Anthropic, read October 13, 2026. Source for the field values, failure definitions, exit code 2 behavior, exclusions, JSON parsing rule and timeout defaults. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19 ↩20 ↩21 ↩22 ↩23 ↩24 ↩25 ↩26 ↩27

  3. @anthropic-ai/claude-code on npm, npm registry publish times queried with npm view, October 13, 2026. Source for the 2.1.287, 2.1.294, 2.1.295 and 2.1.296 publish dates. ↩

  4. Major Updates in Claude Code v2.1.294~v2.1.295 - Addition of hooks onFailure: "block" and OSC 7501 Support, DevelopersIO by Classmethod, October 9, 2026. Source for the observation that claude -p exited 0 for a UserPromptSubmit hook that exits 1, both with and without onFailure: "block". ↩

  5. Hook launch failures (exit 127) are silent and non-blocking: 6,865 skipped guardrail invocations in one session, with no visible signal, issue 81458 in the anthropics/claude-code repository on GitHub, with its em dash written as a colon. Source for the 6,865 count, the exit code, the unquoted path and the hook count, all the reporter's own. The opened and closing dates and the not-planned state were read on the claudeissues.com mirror. ↩ ↩2 ↩3 ↩4

ابقَ على مسار النيرد

رسالتان كلّ أسبوع: شرح معمّق لموضوع واحد يوم الاثنين، وملخّص يوم الخميس للدورات والأدلّة والمقالات والبودكاست والوظائف.

بدون إزعاج. إلغاء الاشتراك في أي وقت.