إعداد Claude Code بعد 2.1.141: كيف اختفت 3 مشكلات في hooks

شرح عملي لتحديث إعداد Claude Code إلى 2.1.141، وكيف تعالج terminalSequence وargs وcontinueOnBlock التنبيهات، واقتباس shell، وحلقات التحقق المتوقفة.

Saturday, September 5, 2026Omid Saffari
إعداد Claude Code بعد 2.1.141: كيف اختفت 3 مشكلات في hooks

خلال 7 أيام فقط، عالج تحديث إعداد Claude Code ثلاثةً من أكثر أخطاء hooks إزعاجًا التي واجهتني في Q1. وفي صباح اليوم التالي، حذفت من مستودع الإدارة ثلاثة حلول التفافية تعتمد على shell.

الأسبوع الذي اختفت فيه ثلاثة أخطاء فعلية

بين 6 مايو و13 مايو 2026، أطلقت Anthropic إصدارات Claude Code من 2.1.132 إلى 2.1.141. وكان معظم الفارق منصبًا على نظام hooks.

انتبهت إلى ذلك لأن مستودعي كان يضم ثلاثة حلول مؤقتة تحمل تعليقات // TODO: remove when claude-code fixes this، ثم حذفتها كلها خلال أسبوع واحد.

وهذه الأخطاء مرتبة بحسب كلفتها عليّ:

أولها كان ضياع تنبيهات سطح المكتب. كان hook من نوع Notification يرسل جرس نظام التشغيل إلى stdout لتنبيهي عند اكتمال عملية طويلة لضغط السياق. نجح ذلك حين كان Claude Code يسيطر على TTY الأمامي، وفشل بصمت في كل وضع آخر: تقسيمات tmux، والطرفيات المدمجة في VS Code، وأجزاء WezTerm عندما يكون التركيز على جزء مجاور. وجاء الإصلاح في صورة terminalSequence ضمن 2.1.141.

ثانيها كان أخطاء الاقتباس في shell. كان hook من نوع Stop يشغّل bash -lc "node post-stop.js --reason '$CLAUDE_STOP_REASON'"، لكن حقل السبب كان يتعطل فور أن يعيد Claude نصًا يتضمن فاصلة علوية. وكان الحل هو صيغة التنفيذ args: string[] في 2.1.139.

أما الثالث فكان رفض PostToolUse الذي ينهي الدور بدل أن يعيد المهمة إلى Claude. اضطررت إلى تعطيل hook للتحقق من المخطط بالكامل، لأن وقت التشغيل كان يتعامل مع قرار block باعتباره خطأً نهائيًا. أضاف 2.1.139 الخيار continueOnBlock، الذي يحوّل الحظر إلى إشارة لإعادة المحاولة مع إدخال السبب في السياق.

ستجد ملف settings.json كاملًا في النهاية، من دون حشو في المنتصف.

إعداد Claude Code: بنية hooks في 90 ثانية

يوفّر Claude Code 2.1.x تسعة أحداث لـ hooks: SessionStart وPreToolUse وPostToolUse وUserPromptSubmit وNotification وStop وSubagentStop وPreCompact وSessionEnd. وكل hook عبارة عن أمر يشغّله وقت التشغيل ضمن بيئة موثقة، مع حمولة تصل عبر stdin. ويحلّل وقت التشغيل stdout الخاص بالـ hook بوصفه JSON، بينما تتحكم حقول بعينها في سلوكه: decision ‏(allow/deny/block)، وreason ‏(نص يُعاد إلى Claude أو يظهر للمستخدم)، وterminalSequence ‏(بايتات خام تُرسل إلى TTY المتحكم)، إلى جانب عدد محدود من الحقول الخاصة بكل حدث.

الخلاصة التي ينبغي ترسيخها: يتعامل وقت التشغيل مع stdout الصادر من hook بوصفه المرجع الحاسم. أي حقل لا يستطيع تحليله لا قيمة له، وأي حقل يستطيع تحليله سيغيّر ما يراه Claude في الدور التالي. هذه هي الفكرة كلها.

terminalSequence: تنبيهات سطح المكتب من دون امتلاك TTY

قبل 2.1.141، كان hook من نوع Notification لديّ كما يلي:

JSON
{
  "hooks": {
    "Notification": [{
      "command": "bash -lc 'printf \"\\a\" && notify-send \"Claude\" \"$CLAUDE_MESSAGE\"'"
    }]
  }
}

كان من المفترض أن يطلق printf "\a" جرس الطرفية. لكنه عمليًا كان يكتب \a إلى واصف الملف الذي ورثته عملية hook، وهذا ليس طرفية المستخدم إلا عندما يكون Claude Code في واجهة التشغيل. وفي تقسيم tmux، حيث يعمل Claude في الجزء 2 بينما أحرر أنا في الجزء 1، لم يكن الجرس يصل إلى الطرفية الخارجية. أما notify-send فكان يعمل، لكنه يستقر في علبة إشعارات GNOME، وغالبًا لا أنتبه إليه.

أضاف 2.1.141 الحقل terminalSequence إلى JSON الذي يخرجه hook عبر stdout. يأخذ وقت التشغيل هذه السلسلة ويكتبها مباشرة إلى جهاز الطرفية المتحكم، متجاوزًا stdio الخاص بعملية hook. وبعد الترقية أصبح الإعداد كما يلي:

JSON
{
  "hooks": {
    "Notification": [{
      "command": "node hooks/notify.mjs"
    }]
  }
}
JavaScript
import { execSync } from "node:child_process";

const payload = JSON.parse(
  await new Response(process.stdin).text()
);

execSync(`notify-send "Claude" ${JSON.stringify(payload.message)}`);

process.stdout.write(JSON.stringify({
  terminalSequence: ""
}));

 هو BEL. يكتبه وقت التشغيل إلى TTY المتحكم، فتطلق الطرفية الخارجية الجرس حتى عندما يعمل Claude في جزء خلفي. تعاملت macOS Terminal وiTerm2 وWezTerm وAlacritty مع ذلك بصورة صحيحة في اختباراتي. ويمرّر tmux إشارة BEL؛ ومع ذلك، أبقِ set -g allow-passthrough on في ملف tmux.conf من أجل OSC 52.

هناك حالة طرفية واحدة تستحق الانتباه: الطرفية المدمجة في VS Code تتجاهل BEL افتراضيًا. اضبط "terminal.integrated.enableBell": true في إعدادات المستخدم إذا أردت تفعيلها هناك.

args: string[] أنهت مشكلات الهروب في أوامر hooks

يعمل hook من نوع Stop عند انتهاء الدور. أستخدمه لتسجيل بيانات الجلسة الوصفية في نسخة Cloudflare D1، حتى أتمكن من البحث في التشغيلات القديمة.

كانت الصيغة السابقة لـ 2.1.139 كما يلي:

JSON
{
  "hooks": {
    "Stop": [{
      "command": "bash -lc \"node scripts/post-stop.js --session $CLAUDE_SESSION_ID --reason '$CLAUDE_STOP_REASON'\""
    }]
  }
}

واجهت ثلاثة أخطاء في الهروب خلال الشهر الأول:

كانت الفواصل العلوية داخل $CLAUDE_STOP_REASON تغلق الوسيط المحاط بعلامتي اقتباس مفردتين، ثم يتعامل shell مع بقية النص بوصفه رموز أوامر. وعندما أعاد Claude سببًا مثل user's request completed، تعطّل hook وضاع سجل الجلسة.

أما علامات backtick في أسماء الأدوات فكانت مشكلة أخرى. عندما انطلق hook وكانت قيمة $CLAUDE_TOOL_NAME تتضمن النص `bash` لأن Claude كتبه في رد، حاول shell تنفيذ المحتوى المحاط بتلك العلامات بوصفه subshell. لم يسبب ذلك ضررًا في هذه الحالة، لكنه احتمال مقلق عمومًا.

ثم هناك Unicode في مطالبات المستخدم. غالبًا ما ينجح انتقال UTF-8 ذهابًا وإيابًا عبر bash -lc، لكن بعض نقاط ترميز CJK، عند اقترانها بإعدادات محلية معينة، كانت تُسقط بايتات بصمت.

أضاف 2.1.139 الأوامر بصيغة exec. مرّر args في صورة مصفوفة نصوص، وسيشغّل وقت التشغيل الأمر مباشرة من دون shell في المنتصف:

JSON
{
  "hooks": {
    "Stop": [{
      "args": [
        "node",
        "scripts/post-stop.js",
        "--session", "$CLAUDE_SESSION_ID",
        "--reason", "$CLAUDE_STOP_REASON"
      ]
    }]
  }
}

يحل وقت التشغيل متغيرات $CLAUDE_* من بيئته الخاصة قبل استدعاء execve. لا shell، ولا استيفاء من shell، ولا اقتباس. ويصل النص user's request completed إلى سكربت Node لديّ بوصفه إدخالًا واحدًا في argv[5]، مطابقًا تمامًا لما أنشأه Claude.

وتزداد أهمية ذلك مع hook من نوع PreToolUse يعمل عند كل استدعاء أداة.

متى ينبغي الإبقاء على صيغة shell؟ عند الحاجة إلى pipes أو redirects أو globbing. الخياران args:[] وcommand:"" متنافيان داخل إدخال hook الواحد؛ لذلك إذا احتجت إلى node x.js | jq | tee log، فالتزم بـ command:"" وتحمّل كلفة الهروب. أما في 90% من hooks، فصيغة exec هي الاختيار الصحيح.

continueOnBlock: رفض PostToolUse يعود فعلًا إلى حلقة التصحيح

هذا هو التحسين الذي انتظرته أطول وقت. أشغّل hook من نوع PostToolUse للتحقق من مخرجات أي استدعاء أداة يكتب إلى القرص: إذا كتب Claude ملف TypeScript، يشغّل hook الأمر tsc --noEmit عليه ويرفض الناتج عند وجود أخطاء في الأنواع.

قبل 2.1.139، كان مسار الرفض معطّلًا:

JSON
{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "command": "node hooks/validate-ts.mjs"
    }]
  }
}

عندما كان validate-ts.mjs يعيد { "decision": "block", "reason": "tsc failed: ..." }، كان وقت التشغيل ينهي الدور. لم يكن Claude يرى السبب، بينما كان المستخدم يتلقى رسالة مبهمة تقول "hook blocked the operation"، ثم يضطر إلى إرسال مطالبة جديدة يدويًا بعد لصق الخطأ فيها. وبعد ثلاث جلسات مباشرة أوقفت فيها هذه المشكلة العمل المنتج، عطّلت hook.

أضاف 2.1.139 الخيار continueOnBlock ضمن إعداد كل hook:

JSON
{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "args": ["node", "hooks/validate-ts.mjs"],
      "continueOnBlock": true,
      "maxAttempts": 3
    }]
  }
}

أصبح قرار block الآن يعيد نص reason إلى سياق Claude بوصفه خطأ في نتيجة الأداة. يرى Claude الرسالة tsc failed: src/api.ts(14,3): error TS2322: Type 'string' is not assignable to type 'number'، ثم يصحح المشكلة بنفسه في الدور التالي. ولا يرى المستخدم شيئًا لأن الحلقة تجري داخليًا.

يمثّل maxAttempts حدًا أقصى صارمًا. فمن دونه، قد يستهلك تحقق غير حتمي — كتحقق يعتمد على API بعيد يتعطل على نحو متقطع — مساحة السياق في محاولات لا تنتهي. أستخدم القيمة ثلاثة. وبعد ثلاث محاولات فاشلة، يصعّد hook الأمر إلى حظر نهائي ويعرضه للمستخدم.

ممارسة ينبغي تجنبها: لا تفعّل continueOnBlock في hooks التي تعتمد قراراتها على حالة مرتبطة بالوقت الفعلي. فالـ hook الذي يرفض الكتابة خلال نافذة نشر سيواصل الدوران ما دام النشر جاريًا عند إعادة محاولة Claude. استخدم شرطًا يستند إلى $CLAUDE_EFFORT أو ضمّن عدادًا للمحاولات في سكربت hook.

الثنائي الإضافي: $CLAUDE_EFFORT وCLAUDE_PROJECT_DIR

تسلّل متغيرا بيئة جديدان إلى هذه الفترة، وكلاهما مفيد بهدوء.

أضاف 2.1.133 المتغير $CLAUDE_EFFORT إلى بيئة hooks. قيمه هي low وmedium وhigh وxhigh، وهي تطابق مستوى الجهد لدى Claude في الدور الحالي. وبذلك يستطيع hook تغيير مساره وفق مستوى الجهد من دون تحليل المطالبة:

JavaScript
const effort = process.env.CLAUDE_EFFORT;

if (effort === "low" || effort === "medium") {
  // skip expensive tsc check on quick edits
  process.stdout.write(JSON.stringify({ decision: "allow" }));
  process.exit(0);
}

// run full tsc --noEmit on high/xhigh

أوفّر نحو 800ms في كل تعديل سريع عندما أتجاوز tsc عند مستوى جهد منخفض. أما في دور تخطيط بمستوى high يكتب فيه Claude دزينة من الملفات، فيستمر التحقق الكامل ويلتقط الأخطاء الفعلية.

أضاف 2.1.139 المتغير CLAUDE_PROJECT_DIR إلى بيئة خوادم MCP التي تعمل عبر stdio ويشغّلها وقت التشغيل. قبل ذلك، كان على خوادم MCP عبر stdio استنتاج جذر مساحة العمل من process.cwd()، وهو ما كان يتعطل إذا شغّل المستخدم Claude Code من مجلد فرعي. والآن يستطيع أي خادم MCP قراءة process.env.CLAUDE_PROJECT_DIR وحل المسارات النسبية إلى مساحة العمل بصورة صحيحة.

إذا كنت تصون خادم MCP، فحدّث آلية حل المسارات في manifest كي تستخدم CLAUDE_PROJECT_DIR مع الرجوع إلى cwd() للعملاء الأقدم. تعديل من سطرين ينهي فئة كاملة من الأخطاء.

ملف settings.json نفسه الذي أستخدمه

هذه هي كتلة الإنتاج من omidsaffari-admin بعد حجب بعض التفاصيل البسيطة. تعتمد ستة عناصر DO وWorkflow واحد على مخرجات hooks لتشغيل تنبيهات سطح المكتب والتحكم في بوابات CI.

JSON
{
  "model": "claude-sonnet-4-7-20260501",
  "permissions": {
    "edit": "ask"
  },
  "hooks": {
    "SessionStart": [{
      "args": ["node", "hooks/session-start.mjs"]
    }],
    "PreToolUse": [{
      "matcher": "Bash",
      "args": ["node", "hooks/gate-bash.mjs"]
    }],
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "args": ["node", "hooks/validate-ts.mjs"],
      "continueOnBlock": true,
      "maxAttempts": 3
    }],
    "Notification": [{
      "args": ["node", "hooks/notify.mjs"]
    }],
    "Stop": [{
      "args": [
        "node",
        "scripts/post-stop.js",
        "--session", "$CLAUDE_SESSION_ID",
        "--reason", "$CLAUDE_STOP_REASON"
      ]
    }],
    "PreCompact": [{
      "args": ["node", "hooks/notify.mjs"]
    }]
  }
}
JSON
{
  "devDependencies": {
    "@anthropic-ai/claude-code": "2.1.141"
  }
}

التثبيت والتحقق:

Bash
pnpm add -D @anthropic-ai/claude-code@2.1.141
claude --version    # expect: 2.1.141
claude config doctor    # expect: 0 hook warnings

أبرز ما هنا: يشكّل continueOnBlock وmaxAttempts ثنائي حلقة إعادة المحاولة الذي وصل في 2.1.139. وتُستخدم صيغة args:[] في كل hook لا يحتاج إلى إمكانات shell، وهذا يشمل جميع hooks في هذا الإعداد. كما يشترك hookا Notification وPreCompact في notify.mjs لأن كليهما يحتاج إلى تنبيهات سطح المكتب بالمخرج نفسه من terminalSequence.

قائمة نشر إعداد Claude Code ومتى يُفضّل التخطي

قبل البدء: التقط نسخة من settings.json الحالي. دوّن أي hooks تستخدم command:"" وأيها يستخدم بالفعل args:[]. ثم احصر كل نوع حدث قمت بتوصيله.

انقل نوع حدث واحدًا في كل مرة، وشغّله 24 ساعة قبل الانتقال إلى التالي. راقب claude config doctor بحثًا عن تحذيرات hooks، وابحث في سجلاتك عن النصين decision وreason للتأكد من أن وقت التشغيل يرى ما تتوقعه.

هذا هو الترتيب الذي أتّبعه:

  1. رقِّ الحزمة إلى 2.1.141.
  2. حوّل hook واحدًا من command:"" إلى args:[]، ثم تحقق من أنه يعمل.
  3. أضف terminalSequence إلى hook من نوع Notification، واختبر الجرس من جزء tmux يعمل في الخلفية.
  4. أضف continueOnBlock إلى hook من نوع PostToolUse الأكثر إزعاجًا لديك، وراقب جلسة فعلية واحدة للتأكد من أن Claude يرى السبب ويصحح نفسه.
  5. انقل بقية hooks إلى args:[] على دفعات.

تجاوز الترقية إذا كنت تستخدم تثبيتًا مُدارًا ومثبتًا على إصدار تحدده حزمة SDK رئيسية لم تعتمد 2.1.141 بعد، أو إذا كنت تعتمد على حقل hook صنّفه 2.1.141 مهملًا. وحتى وقت كتابة هذا المقال، لم تتضمن نافذة مايو أي تغيير كاسر للحقول الحالية؛ لكن ثبّت الإصدار واختبره في فرع قبل تعميمه على فريقك.

إذا أردت الدليل الكامل لتثبيت الإصدارات، واستراتيجية hooks، وبناء هيكل المشروع الذي أستخدمه عبر ستة وكلاء في بيئة الإنتاج، فستجد ذلك في قائمة إعداد Claude Code وCodex من البداية إلى النهاية. وهي تتضمن أنماط settings.json نفسها، إلى جانب هيكل الوكلاء (Workflows وDOs وارتباطات Vectorize) الذي تتحكم فيه hooks.

وللاطلاع على المقال الموازي حول تشغيل هؤلاء الوكلاء داخل بيئات معزولة بعيدة، راجع بيئات Cursor Cloud Agent مقارنةً بـ Cloudflare Workers. أما خلفية منظومة الإنتاج وراء ملف settings.json هذا، فستجدها في مقال مهندس Cloudflare ذي إنتاجية 100x.

هل يعمل terminalSequence داخل tmux؟

نعم، مع ملاحظة واحدة. يكتب وقت التشغيل التسلسل إلى جهاز الطرفية المتحكم، ويمرّره tmux إلى الطرفية الخارجية ما دام set -g allow-passthrough on موجودًا في ملف tmux.conf، أو إذا كان التسلسل مجرد BEL عادي (). تمر BEL دون شروط، أما تسلسلات OSC فتحتاج إلى تفعيل passthrough.

هل يمكن استخدام args:[] وcommand:'' معًا في إعداد hook نفسه؟

لا. الخياران متنافيان في كل إدخال hook. اختر صيغة exec ‏(args:[]) للأوامر التي تُشغَّل مباشرة، وصيغة shell ‏(command:"") لأي أمر يحتاج إلى pipes أو redirects أو globbing. وإذا احتجت إلى الجمع بينهما، فاكتب سكربتًا وسيطًا يستخدم صيغة exec ويحتوي إمكانات shell داخليًا.

هل سيستمر continueOnBlock في حلقة لا نهائية إذا كرر Claude خطأ التحقق نفسه؟

ليس عند ضبط maxAttempts. يضع وقت التشغيل حدًا للمحاولات عند هذا العدد، ثم يصعّد الأمر إلى حظر نهائي. ومن دون maxAttempts، نعم، قد تستهلك مساحة السياق بسبب أداة تحقق غير حتمية. اضبطه دائمًا. ثلاثة حد افتراضي معقول لعمليات التحقق الشبيهة بفحص الأنواع، بينما واحد هو الأنسب لأي عملية تعتمد على حالة بعيدة.

هل يتوفر $CLAUDE_EFFORT في كل حدث hook؟

نعم، اعتبارًا من 2.1.133، يُضاف إلى بيئة كل hook يشغّله وقت التشغيل. وتعكس القيمة مستوى الجهد للدور الحالي؛ لذلك سيرى SessionStart مستوى الجهد الذي بدأ به المستخدم، بينما سيرى PostToolUse المستوى الذي كان نشطًا عند تشغيل الأداة.

ماذا يتعطل عند الرجوع إلى 2.1.138؟

تتوقف terminalSequence وargs:[] وcontinueOnBlock وCLAUDE_PROJECT_DIR كلها عن العمل بصمت. يتجاهل وقت التشغيل حقول JSON غير المعروفة ويعود إلى تحليل command:"". ستستمر hooks في العمل، لكن السلوكيات الجديدة لن تعمل. اختبر مسار الرجوع في فرع قبل الاعتماد عليه.

آخر تحديث

5 سبتمبر 2026

التصنيفBuild

فضّل هذا الموقع في Google

إضافة omidsaffari.com كمصدر مفضّل في بحث Google

اجعل omidsaffari.com مصدرًا مفضّلًا، وسيرفعه Google لك في Top Stories وAI Overviews وAI Mode.

المزيد من Build

عرض كل مقالات Build
النشرة البريدية

رسالة واحدة، كل يوم أحد. أنظمة تعمل، لا آراء ساخنة.

سجلات بناء، وأنظمة قيد التشغيل، وملاحظات ميدانية من إدارة محفظة مشاريع ذكاء اصطناعي.

أسبوعية. بلا إزعاج. يمكنك إلغاء الاشتراك متى شئت.