ai-ml

إضافة موافقة العنصر البشري في الحلقة لـ Claude Agent

١٣ يوليو ٢٠٢٦

Add Human-in-the-Loop Approval to Claude Agent SDK (2026)

تقوم بوابة الموافقة بتدخل بشري (human-in-the-loop) بإيقاف عميل الذكاء الاصطناعي مؤقتًا قبل تشغيل استدعاء أداة خطيرة — مثل Bash أو Write — وتنتظر شخصًا للموافقة عليها أو رفضها. في Claude Agent SDK، يمكنك بناء ذلك باستخدام استدعاء راجع (callback) من نوع canUseTool، وخطاف (hook) من نوع PreToolUse، وخطاف لسجل التدقيق1.

ملخص

ستقوم ببناء سكربت TypeScript مكون من حوالي 90 سطرًا يغلف @anthropic-ai/claude-agent-sdk@0.3.150 بثلاث طبقات من التحكم: خطاف PreToolUse يوافق تلقائيًا على أدوات القراءة فقط ويحظر تمامًا الكتابة في ملفات الأسرار بغض النظر عما يقرره أي شخص لاحقًا، واستدعاء راجع canUseTool يطلب من البشر في الطرفية (terminal) قبل تشغيل Bash أو Write أو Edit، وخطاف PostToolUse يضيف كل استدعاء أداة إلى سجل تدقيق بتنسيق JSON-lines. بيئة التشغيل: Node 18+ (يوصى بـ Node 22 أو 24 LTS)، إصدار SDK هو @anthropic-ai/claude-agent-sdk@0.3.150. وقت البناء: 20-25 دقيقة.

ما ستتعلمه

  • كيف يقرر تدفق تقييم الأذونات المكون من خمس خطوات في SDK ما إذا كان استدعاء الأداة يحتاج إلى تدخل بشري أصلاً
  • كيفية كتابة استدعاء راجع canUseTool يعرض للمستخدم ما يريد Claude فعله ويعيد allow/deny
  • كيفية إضافة خطاف PreToolUse يحظر فئة كاملة من الاستدعاءات الخطيرة قبل استدعاء canUseTool
  • كيفية تسجيل كل استدعاء أداة باستخدام خطاف PostToolUse، بغض النظر عما إذا تمت الموافقة عليه تلقائيًا أو بواسطة إنسان
  • الفرق بين خطافات PreToolUse واستدعاء canUseTool الراجع، ومتى تستخدم كل منهما
  • أي أوضاع أذونات (default، dontAsk، acceptEdits، bypassPermissions، plan، auto) تتخطى المطالبة البشرية تمامًا — ولماذا يهم ذلك لعملاء الإنتاج

المتطلبات الأساسية

  • Node.js 18.0.0 أو أحدث — يحدد ملف package.json الخاص بـ SDK القيمة "engines": {"node": ">=18.0.0"}2؛ تم بناء هذا البرنامج التعليمي والتحقق من أنواعه باستخدام Node 22.22.3
  • @anthropic-ai/claude-agent-sdk@0.3.150 (أحدث إصدار latest على npm وقت كتابة هذا التقرير)2، بالإضافة إلى تبعياته الزميلة zod@^4.0.0 و @anthropic-ai/sdk@>=0.93.0 و @modelcontextprotocol/sdk@^1.29.02
  • TypeScript 5.x مع تفعيل strict و noUncheckedIndexedAccess
  • مفتاح ANTHROPIC_API_KEY من منصة Claude إذا كنت تريد تشغيل حلقة العميل فعليًا من البداية إلى النهاية (الكود في هذا البرنامج التعليمي تم التحقق من أنواعه مقابل SDK الحقيقي، ولكن تشغيله يكلف قدرًا صغيرًا من استخدام API — راجع قسم التحقق لمعرفة ما تم تنفيذه وما لم يتم تنفيذه)

لماذا لا يكفي وضع الأذونات؟

يوفر Claude Agent SDK بالفعل أوضاع أذونات — default، dontAsk، acceptEdits، bypassPermissions، plan، و (في TypeScript فقط) auto1. يقوم bypassPermissions بتشغيل كل أداة دون أي مطالبات على الإطلاق؛ بينما يوافق acceptEdits تلقائيًا على تعديلات الملفات وأوامر نظام الملفات مثل mkdir/rm/mv. لا يوفر أي منهما نقطة تفتيش بشرية — فهي سياسات عامة وليست مراجعة لكل استدعاء. وضع default هو الوضع الذي يصل فعليًا إلى الإنسان: فهو يمر عبر الخطافات، وقواعد الرفض، وقواعد السماح، وفقط بعد ذلك يستدعي استدعاء canUseTool الراجع الخاص بك1. هذا الاستدعاء الراجع — بالإضافة إلى خطاف أو اثنين لمنع الاستدعاء الراجع من أن يكون خط دفاعك الوحيد — هو ما تُبنى منه بوابة موافقة حقيقية.

الخطوة 1: إعداد المشروع

mkdir agent-approval-gate && cd agent-approval-gate
npm init -y
npm install @anthropic-ai/claude-agent-sdk@0.3.150 zod@4
npm install -D TypeScript@5 @types/node@22
npx tsc --init --target ES2022 --module NodeNext --moduleResolution NodeNext --strict

أضف "type": "module" إلى package.json حتى يتعامل Node مع مخرجات .js كـ ESM (يتم شحن SDK نفسه كـ "type": "module"، ويتوقع tsc --moduleResolution NodeNext أن يتطابق المشروع المستهلك مع ذلك)2.

الخطوة 2: فهم تدفق تقييم الأذونات

قبل كتابة الاستدعاء الراجع، يجب أن تعرف ما الذي يُطلب منه تقريره فعليًا. عندما يطلب Claude أداة، يتحقق SDK من الأذونات بهذا الترتيب الثابت1:

  1. الخطافات (Hooks) — يتم تشغيل خطافات PreToolUse أولاً ويمكنها السماح أو الرفض أو التمرير
  2. قواعد الرفض — من disallowedTools أو settings.json؛ تظل هذه القواعد سارية حتى في وضع bypassPermissions
  3. وضع الأذونات — يوافق bypassPermissions على كل ما يصل إلى هذه الخطوة؛ بينما يوافق acceptEdits على عمليات الملفات؛ ويوافق plan على أدوات القراءة فقط ويحظر الباقي
  4. قواعد السماح — من allowedTools أو settings.json
  5. استدعاء canUseTool الراجع — يتم الوصول إليه فقط إذا لم يحل أي شيء أعلاه الاستدعاء؛ في وضع dontAsk يتم تخطي هذه الخطوة وتُرفض الأداة تمامًا

الخلاصة العملية: canUseTool ليست "البوابة" الوحيدة، بل هي البوابة الأخيرة. الأداة التي تغطيها بالفعل قاعدة سماح، أو وضع مثل acceptEdits، لا تصل أبدًا إلى الاستدعاء الراجع الخاص بك3. إذا كنت بحاجة إلى قاعدة تنطبق حرفيًا على كل استدعاء أداة بغض النظر عن الوضع، فأنت بحاجة إلى خطاف PreToolUse، وليس canUseTool.

الخطوة 3: كتابة خطاف الحماية الحتمي

يتم تشغيل الخطافات قبل بقية تدفق الأذونات، لذا فهي المكان المناسب للقواعد التي لا ينبغي أبدًا تخطيها — ولا حتى بواسطة إنسان ينقر على "موافقة" بسرعة كبيرة. يقوم هذا الخطاف بأمرين: يحظر تمامًا أي كتابة ملف تلمس مسار أسرار، ويوافق تلقائيًا على أدوات القراءة فقط حتى لا يُطلب من الإنسان مباركة استدعاء Grep.

import type { HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";

const READ_ONLY_TOOLS = ["Read", "Glob", "Grep"];

const blockSecretWrites: HookCallback = async (input) => {
  if (input.hook_event_name !== "PreToolUse") return {};
  const preInput = input as PreToolUseHookInput;
  const toolInput = preInput.tool_input as Record<string, unknown>;
  const filePath = (toolInput?.file_path as string) ?? "";

  // Matches .env, .env.local, .env.production, .envrc, and anything
  // under a /secrets/ directory.
  if (/\.env(rc|\..+)?$|\/secrets\//.test(filePath)) {
    return {
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "deny",
        permissionDecisionReason: `${filePath} matches a protected-secrets pattern`,
      },
    };
  }

  if (READ_ONLY_TOOLS.includes(preInput.tool_name)) {
    return {
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "allow",
        permissionDecisionReason: "read-only tool auto-approved",
      },
    };
  }

  return {};
};

هناك تفصيلان مهمان هنا، كلاهما تم التحقق منهما مقابل تعريفات أنواع SDK الخاصة، وليس فقط الوثائق: يحمل PreToolUseHookInput كلاً من tool_name و tool_input (المكتوب كـ unknown، ومن هنا جاء التحويل)4، ويجب أن يعيش كل من updatedInput/permissionDecision داخل hookSpecificOutput — يتم تجاهل permissionDecision المجرد في المستوى الأعلى بصمت3. إرجاع {} يعني "لا رأي، استمر في التقييم" — ولا يعني الرفض.

ثغرة في التعبيرات النمطية (regex) تستحق المعرفة: النمط أعلاه يلتقط .env، و .env.local، و .envrc، لكن النمط البسيط /\.env(\..+)?$/ (بدون تبديل rc) يغفل عن .envrc تماماً — لقد اكتشفت هذا عن طريق تشغيل كلا النمطين مقابل قائمة حالات اختبار صغيرة قبل النشر (.env ← محظور، .env.local ← محظور، .envrc ← محظور، notes.txt ← مسموح، src/environment.ts ← مسموح). إذا كان فريقك يستخدم اصطلاحاً مختلفاً للأسرار (.secrets.yaml، credentials.json)، فقم بتوسيع النمط بدلاً من الوثوق بهذا النمط حرفياً.

الخطوة 4: كتابة استدعاء الموافقة canUseTool

كل ما لم يتم حله بواسطة الخطاف (hook) أعلاه — في هذا العرض التوضيحي، يشمل ذلك Bash و Write و Edit — ينتقل إلى canUseTool. يقوم هذا الاستدعاء بطباعة ما يريد Claude فعله وينتظر إجابة y/n في الطرفية (terminal)5:

import * as readline from "node:readline/promises";
import type { CanUseTool } from "@anthropic-ai/claude-agent-sdk";

function prompt(question: string): Promise<string> {
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
  return rl.question(question).finally(() => rl.close());
}

const approveInTerminal: CanUseTool = async (toolName, input) => {
  console.log(`\nClaude wants to use: ${toolName}`);
  console.log(JSON.stringify(input, null, 2));

  const answer = await prompt("Approve this tool call? (y/n): ");
  if (answer.trim().toLowerCase() === "y") {
    return { behavior: "allow", updatedInput: input };
  }
  return { behavior: "deny", message: "User denied this action in the approval gate" };
};

نوع الإرجاع هو PermissionResult، والذي تعرفه الأنواع المجمعة لـ SDK كاتحاد متميز (discriminated union): { behavior: "allow", updatedInput?, updatedPermissions?, ... } أو { behavior: "deny", message: string, interrupt?: boolean, ... }4. الـ message عند الرفض ليست مجرد سجلات — بل يتم إرجاعها إلى Claude، الذي يمكنه قراءتها ومحاولة نهج مختلف5. الوسيط الثالث للاستدعاء يكشف أيضاً عن suggestions (إدخالات تحديث أذونات جاهزة يمكنك إعادتها لخيار "السماح بهذا دائماً") و toolUseID للربط مع أحداث PostToolUse4 — هذا العرض التوضيحي لا يستخدمها، لكنها موجودة بمجرد أن تتجاوز مرحلة طلب y/n البسيط.

شيء واحد يسهل تفويته: يتم الوصول إلى canUseTool فقط للأدوات التي لم يتم حلها مسبقاً. إذا قمت بضبط permissionMode: "acceptEdits"، فلن يتم تشغيل هذا الاستدعاء أبداً لـ Write أو Edit — حيث يوافق الوضع تلقائياً عليها قبل أن يصل التقييم إلى هذه المرحلة1.

الخطوة 5: كتابة خطاف سجل التدقيق

يتم تشغيل PostToolUse بعد انتهاء الأداة، سواء تمت الموافقة عليها بواسطة الخطاف، أو بواسطة إنسان، أو بواسطة وضع الأذونات — مما يجعله المكان المناسب لمسار التدقيق الذي لا يعتمد على كيفية الموافقة على الاستدعاء3.

import { appendFile } from "node:fs/promises";
import type { HookCallback, PostToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";

const auditLog: HookCallback = async (input) => {
  if (input.hook_event_name !== "PostToolUse") return {};
  const postInput = input as PostToolUseHookInput;
  const line = JSON.stringify({
    ts: new Date().toISOString(),
    tool: postInput.tool_name,
    input: postInput.tool_input,
  });
  await appendFile("audit.log", line + "\n");
  return {};
};

يكتب هذا كائن JSON واحداً لكل سطر — وهو تنسيق يمكنك متابعته عبر tail -f | jq خلال جلسة مباشرة أو إدخاله في خط أنابيب السجلات لاحقاً.

الخطوة 6: ربط كل شيء معاً

import { query } from "@anthropic-ai/claude-agent-sdk";

async function main() {
  for await (const message of query({
    prompt:
      "Create a file named notes.txt with the text 'hello from the agent', " +
      "then run `ls -la` to confirm it exists.",
    options: {
      permissionMode: "default",
      allowedTools: ["Bash", "Write", "Edit", "Read", "Glob", "Grep"],
      hooks: {
        PreToolUse: [{ hooks: [blockSecretWrites] }],
        PostToolUse: [{ hooks: [auditLog] }],
      },
      canUseTool: approveInTerminal,
    },
  })) {
    if (message.type === "assistant") {
      for (const block of message.message.content) {
        if (block.type === "text") console.log(block.text);
      }
    }
    if ("result" in message) {
      console.log("\n--- result ---");
      console.log((message as { result: string }).result);
    }
  }
}

main();

حقل prompt في query() يقبل سلسلة نصية عادية هنا — لست بحاجة إلى حل المولد المتدفق (streaming-generator) الذي يتطلبه can_use_tool في SDK الخاص بـ Python في بعض الحالات5؛ هذا الحل خاص بمسار طلب المولد المحدود في Python ولا ينطبق على شكل استدعاء TypeScript هذا. permissionMode: "default" هو ما يجعل الوصول إلى canUseTool ممكناً على الإطلاق — اضبطه على bypassPermissions وسيصبح الاستدعاء أعلاه كوداً غير مستخدم، لأن كل أداة تتجاوز خطوة وضع الأذونات قبل أن يصل التقييم إليها1.

التحقق

ما تم تشغيله بالفعل: تم حفظ البرنامج النصي الكامل (جميع المقاطع الأربعة مجتمعة، حوالي 95 سطراً) في مشروع حقيقي، وتم تثبيته مقابل حزمة @anthropic-ai/claude-agent-sdk@0.3.150 الحقيقية من npm، وفحصه برمجياً باستخدام tsc --strict --noUncheckedIndexedAccess. يتم تجميعه بنجاح. إحدى النتائج الحقيقية من ذلك الفحص: ملف sdk.d.ts الخاص بـ SDK في هذا الإصدار يحتوي على أخطاء أنواع داخلية غير متعلقة (Cannot find name 'RemoteControlHandle'، وعدة أنواع SDKControl*Request مفقودة) والتي تظهر ما لم يقم ملف tsconfig.json الخاص بك بضبط "skipLibCheck": true — هذا ليس خطأ في الكود أعلاه، بل هو مشكلة موجودة مسبقاً في الأنواع المشحونة مع الحزمة، وكل مشروع يستخدم إصدار SDK هذا يحتاج إلى skipLibCheck: true (أو ملف d.ts مصحح) للفحص البرمجي بنجاح.

تم تنفيذ التعبير النمطي blockSecretWrites بشكل مستقل مقابل ستة مسارات اختبار:

المسارمحظور؟
.envنعم
.env.localنعم
.envrcنعم
config/secrets/db.ymlنعم
notes.txtلا
src/environment.tsلا

ما لم يتم تشغيله: استدعاء query() المباشر مقابل Claude API الحقيقي. يتطلب القيام بذلك مفتاح ANTHROPIC_API_KEY مدفوعاً، وهو ما لا يملكه خط أنابيب الكتابة الآلي هذا. إذا كان لديك مفتاح، فقم بتشغيل:

export ANTHROPIC_API_KEY=sk-ant-...
npx tsx approval-gate.ts

يجب أن ترى طلبين لـ canUseTool (واحد لـ Write، والآخر لـ Bash) وبعد الموافقة على كليهما، ملف audit.log يحتوي على سطرين JSON وملف notes.txt في دليل العمل الخاص بك.

استكشاف الأخطاء وإصلاحها

canUseTool لا يعمل أبداً، حتى بالنسبة لـ Bash. تحقق من permissionMode الخاص بك. كل من bypassPermissions و acceptEdits يقومان بحل استدعاءات الأدوات قبل الوصول إلى خطوة الاستدعاء1. إذا كنت تريد أن يمر كل استدعاء أداة لم يتم حله بواسطة الخطاف عبر إنسان، فاستخدم permissionMode: "default" (أو اتركه غير محدد، لأن default هو الوضع الافتراضي لـ SDK).

تعديل updatedInput من الخطاف لا يسري مفعوله. يجب عليك أيضاً إرجاع permissionDecision: "allow" (أو "ask") في نفس كائن hookSpecificOutput — يتم تجاهل updatedInput بمفرده، أو updatedInput في المستوى الأعلى بدلاً من داخل hookSpecificOutput3.

الخطاف لا يعمل على الإطلاق. أسماء الأحداث حساسة لحالة الأحرف — PreToolUse، وليس preToolUse أو pretooluse3. تحقق أيضاً من أن matcher الخاص بك (إذا قمت بضبطه) ليس محدود النطاق للغاية؛ حيث يعمل المطابق المحذوف لكل أداة من ذلك النوع من الأحداث.

تجاوز وكيل فرعي (subagent) بوابة الموافقة تماماً. إذا كانت خيارات المستوى الأعلى لديك تضبط permissionMode: "bypassPermissions"، فإن الوكلاء الفرعيين يرثون هذا الوضع ولا يمكن تجاوزه لكل وكيل فرعي — فهم يحصلون على وصول كامل ومستقل للأدوات دون أي طلبات موافقة1. لا تضبط bypassPermissions في المستوى الأعلى إذا كان أي وكيل فرعي قد يلمس شيئاً حساساً.

أعطى كل من الخطاف (hook) و canUseTool إجابات مختلفة. لا يمكن أن يتعارضا بمعنى أن أحدهما يلغي الآخر بعد وقوع الحدث — فتدفق التقييم متسلسل ويتوقف عند أول خطوة تحسم الاستدعاء. ولكن إذا قمت بتسجيل خطافات متعددة لنفس الحدث واختلفت فيما بينها، فإن deny يتفوق على defer، والذي بدوره يتفوق على ask، والذي يتفوق على allow3.

كيف يختلف خطاف PreToolUse عن canUseTool؟

يتم تشغيل خطاف PreToolUse أولاً، لكل استدعاء أداة من النوع المطابق، في كل وضع صلاحيات باستثناء عندما تلغيه قاعدة deny من disallowedTools — فهو الطبقة المخصصة للقواعد التي يجب ألا تكون قابلة للتخطي أبداً13. أما canUseTool فهو استدعاء راجع (callback) وقت التشغيل لا يتم تفعيله إلا لما لم تحسمه الخطافات، وقواعد المنع، ووضع الصلاحيات، وقواعد السماح بالفعل — فهو الطبقة المخصصة للقرارات التفاعلية الخاصة بالجلسة والتي تتطلب تدخلاً بشرياً1. تستخدم معظم الوكلاء البرمجية في بيئة الإنتاج التي تحتاج إلى الحتمية والإشراف البشري معاً خطافاً للثوابت (حظر /etc، حظر الأسرار، الموافقة التلقائية على أدوات القراءة فقط) و canUseTool لكل ما يتبقى.

الخطوات التالية ومزيد من القراءة

يبني هذا البرنامج التعليمي طبقة الموافقة فوق الأدوات المدمجة في SDK وهي (Bash، Write، Edit، Read، Glob، Grep). إذا كان وكيلك يحتاج إلى استدعاء خدمات خارجية بدلاً من الملفات المحلية، فإن نفس استدعاء canUseTool وخطافات PreToolUse تنطبق على أدوات MCP التي تعرضها عبر stdio — تتبع أسماء أدوات MCP ببساطة نمط mcp__<server>__<action> للمطابقات. إذا كنت تقوم بتشغيل خادم MCP يحتاج هو نفسه إلى مصادقة، فراجع البرنامج التعليمي لخادم MCP للإنتاج مع OAuth و HTTP القابل للبث. وإذا كنت تبني حلقة استدعاء الأدوات الأساسية من رسائل Messages API الخام بدلاً من Agent SDK، فإن البرنامج التعليمي لحلقة الوكيل لاستخدام أدوات Claude يغطي معالجة tool_use/tool_result في ذلك المستوى الأدنى.

لإخطار إنسان خارج النطاق بدلاً من الحظر في واجهة الأوامر (terminal)، يوجد خطاف PermissionRequest الخاص بـ SDK خصيصاً لتوجيه أحداث "Claude ينتظر الموافقة" إلى Slack أو البريد الإلكتروني أو خدمة إشعارات الدفع3 — وهو أمر يستحق الاستخدام بمجرد أن تحتاج بوابة الموافقة الخاصة بك إلى العمل لفريق، وليس مجرد جلسة واجهة أوامر واحدة.

الحواشي السفلية

  1. Anthropic، "تكوين الصلاحيات" — https://platform.claude.com/docs/en/agent-sdk/permissions (ترتيب تقييم الصلاحيات، جدول أوضاع الصلاحيات، تحذير توريث الوكيل الفرعي bypassPermissions؛ تم الجلب في 13-07-2026) 2 3 4 5 6 7 8 9 10

  2. سجل npm، @anthropic-ai/claude-agent-sdkhttps://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/latest (الإصدار 0.3.150، engines.node >=18.0.0، التبعيات النظيرة zod ^4.0.0 / @anthropic-ai/sdk >=0.93.0 / @modelcontextprotocol/sdk ^1.29.0، المدمج claudeCodeVersion 2.1.150؛ تم الجلب في 13-07-2026 مباشرة من السجل API. أفاد ملخص بحث ويب بشكل منفصل عن إصدار أحدث "0.3.205"، ولكن لم يتسن تأكيد ذلك بشكل مستقل مقابل نقطة نهاية latest الخاصة بالسجل في وقت الجلب، لذا فإن هذا البرنامج التعليمي مثبت على الإصدار 0.3.150 المؤكد من السجل.) 2 3 4

  3. Anthropic، "اعتراض والتحكم في سلوك الوكيل باستخدام الخطافات" — https://code.claude.com/docs/en/agent-sdk/hooks (جدول أحداث الخطاف، شكل PreToolUseHookInput/PreToolUseHookSpecificOutput، أولوية deny > defer > ask > allow، خطاف PermissionRequest؛ تم الجلب في 13-07-2026) 2 3 4 5 6 7 8

  4. تعريفات الأنواع المجمعة لـ @anthropic-ai/claude-agent-sdk@0.3.150، sdk.d.ts (إعلانات الأنواع CanUseTool، PermissionResult، PermissionMode، PreToolUseHookInput؛ تم التثبيت من npm وفحصها مباشرة، 13-07-2026) 2 3

  5. Anthropic، "التعامل مع الموافقات ومدخلات المستخدم" — https://platform.claude.com/docs/en/agent-sdk/user-input (شرح callback لـ canUseTool، أشكال استجابة PermissionResultAllow/Deny، متطلبات وضع الـ streaming في Python لـ can_use_tool؛ تم جلب البيانات في 2026-07-13) 2 3