Free

نقل session recording الخاص بـ Claude Code إلى مشروع ثانٍ

نقل نظام تسجيل how2claude إلى مشروع Rails آخر: 4 ملفات، 5 خطوات، طبقات hook.


how2claude يحتوي مجموعة hooks تُسجِّل جلسات Claude Code تلقائياً — تبدأ التسجيل عند بدء العمل، تُضيف checkpoint مع كل commit، وعند انتهاء الجلسة تستخرج prompts / bash / قوائم التعديل إلى docs/notes/<feature>/raw.md. مقال let-claude-record-itself شرح كيف بُني.

المشكلة: مشروعي الآخر (smarts، موقع توثيق للعقود الذكية) لا يحتوي شيئاً من ذلك. في كل مرة أريد أن أكتب مقالاً لاحقاً، أنبش git log مع الذاكرة وأشعر أن الجزء الأفضل يضيع مني. هذا المقال يحكي عن نقل نظام التسجيل إلى هناك — في المجموع 4 ملفات، 5 دقائق — لكن خلال الطريق ظهرت فكرة حقيقية عن طبقات الـ hooks: hooks الخاصة بـ Claude Code و hooks الخاصة بـ git تعمل في طبقتين مختلفتين تماماً، وما يتم التقاطه عند خلط الأدوات (مثل Amp + Claude Code) يعتمد على الطبقة التي تُركِّب فيها.


4 ملفات، صورة واحدة

كل ما في how2claude متعلق بالتسجيل:

الملف الطبقة الدور
bin/recording-state script Python helper، يُدير دورة حياة .state.json
bin/extract-session-notes script Python helper، يقرأ transcript الخاص بـ Claude Code → يكتب في raw.md
.claude/settings.local.json hook Claude Code PostToolUse / Stop يُشغِّل الـ scripts أعلاه
.git/hooks/post-commit hook git كل commit يستدعي recording-state commit لعمل checkpoint
.gitignore ضبط الضجيج يُبقي docs/notes/ خارج الـ repo (الملاحظات خاصة/مؤقتة)

4 قطع، 4 طبقات مختلفة. هذا التمييز يعود لاحقاً.

الفكرة الجوهرية: طبقات الـ hooks تُحدِّد ما يُلتقط

نظامان من الـ hooks يعملان في الوقت نفسه، لكن نطاقاهما مختلفان جداً:

Hooks الخاصة بـ Claude Code (مُعرَّفة في .claude/settings.local.json):
- النطاق: تُطلَق فقط داخل أداة Claude Code
- المُطلِقات: PostToolUse / Stop / PreToolUse — أحداث دورة حياة Claude Code
- المعلومات المتوفرة: اسم الـ tool، المعاملات، transcript_path (jsonl كامل للجلسة) — أشياء يعرفها Claude Code وحده

Hooks الخاصة بـ git (scripts shell تحت .git/hooks/):
- النطاق: تُطلَق في كل حدث git، بصرف النظر عمّن أطلق git
- المُطلِقات: post-commit / pre-push / إلخ
- المعلومات المتوفرة: ما يعرفه git نفسه (sha، المؤلف، الفرع، الـ diff)

النتيجة العملية: كتابة كود + commit داخل Claude Code تُطلق الطبقتين — معلومات الجلسة ومعلومات الـ commit تهبط في raw.md. الانتقال إلى Amp (أو Cursor، أو الكتابة يدوياً) للكتابة + commit تُطلق hook git فقط — raw.md يحصل على هيكل الـ commit لكن لا يحصل على prompts / bash / تفاصيل تعديل الجلسة.

هذا ليس bug — هذا قيد تصميم كل أداة. أن تريد تفاصيل على مستوى الجلسة تحت جميع الأدوات يعني تركيب طبقة hook خاصة بك لكل أداة. الشبكة الاحتياطية لـ git تعطيك "ماذا فُعِل"؛ لا تعطيك "ماذا فُكِّر فيه، وأين انكسر".

دليل الاختيار:
- الهيكل المحايد للأداة (معلومات الـ commit، تغييرات الكود) → ضعه في hook git
- اللحم الخاص بـ Claude Code (prompts كاملة، المنطق) → ضعه في hook Claude Code
- الاثنان معاً → ركِّب في الطبقتين

النقل في 5 خطوات

تركيب نفس إعدادات التسجيل في smarts (/home/bob/Work/smarts، مشروع Rails).

1. نسخ الـ scripts الاثنين بـ Python

mkdir -p /home/bob/Work/smarts/bin
cp /home/bob/Work/how2claude/bin/recording-state \
   /home/bob/Work/smarts/bin/recording-state
cp /home/bob/Work/how2claude/bin/extract-session-notes \
   /home/bob/Work/smarts/bin/extract-session-notes
chmod +x /home/bob/Work/smarts/bin/{recording-state,extract-session-notes}

الـ scripts بلا تعديل — تستخدم متغير البيئة $CLAUDE_PROJECT_DIR لتحديد موضع الكتابة:

def project_dir():
    return os.environ.get("CLAUDE_PROJECT_DIR") or os.getcwd()

def state_path():
    return pathlib.Path(project_dir()) / "docs/notes/.state.json"

هذا هو التجريد المفتاح: Claude Code يضبط CLAUDE_PROJECT_DIR تلقائياً عند إطلاق hook؛ نحن نضبطه يدوياً داخل post-commit الخاص بـ git. كلا الجانبين يحترم نفس متغير البيئة، ولا يحتاج الـ script إلى أن يحدد "في أي مشروع أنا".

2. إنشاء .claude/settings.local.json

قرار هنا: انقل الـ hooks فقط، لا تنقل قائمة permissions.

settings.local.json الخاص بـ how2claude فيه أكثر من 100 إدخال permissions.allow — كلها خاصة بـ how2claude (curl localhost:3000، bin/rails runner، kamal app exec). لا معنى لجرّها إلى smarts. smarts سيجمع permissions الخاصة به عضوياً أثناء الاستخدام.

الـ hooks أنماط — متطابقة عبر المشاريع. الـ permissions حالة مشروع — مختلفة عبر المشاريع.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/bin/recording-state maybe-start"
          }
        ]
      },
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/bin/recording-state maybe-stop"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "$CLAUDE_PROJECT_DIR/bin/extract-session-notes" }
        ]
      }
    ]
  }
}

ثلاث نقاط إطلاق:
- تعديل ملف → محاولة بدء التسجيل (maybe-start يتفحص ذاته: إن كان يُسجِّل أصلاً فيتخطى، إن كان الشجرة نظيفة فيتخطى أيضاً)
- تنفيذ أمر bash → محاولة الإيقاف (maybe-stop صارم: يوقف فقط عند اجتماع "بدأ تلقائياً و عاد إلى master و الشجرة نظيفة" معاً)
- انتهاء الجلسة → استخراج transcript إلى raw.md

3. إنشاء .git/hooks/post-commit

3 أسطر:

#!/bin/bash
ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
CLAUDE_PROJECT_DIR="$ROOT" "$ROOT/bin/recording-state" commit || true

تصدير CLAUDE_PROJECT_DIR=$ROOT يدوياً هو النقطة الدقيقة التي يُجسَّر فيها عالم git مع عالم Claude Code عبر نفس متغير البيئة. || true يضمن أن الـ hook لا يعترض commit أبداً.

chmod +x /home/bob/Work/smarts/.git/hooks/post-commit

4. أضِف docs/notes/ إلى .gitignore

# Session recording notes (transient, for article material)
docs/notes/

الملاحظات مؤقتة + خاصة — لا تريد أن يُرفع raw.md ضمن الـ PRs؛ لا تريد أن يُلوِّث .state.json ناتج git status. gitignore الخاص بـ how2claude يعاملها بالطريقة نفسها.

5. اختبار دخاني (مع مفاجأة صغيرة)

أطلق maybe-start يدوياً فور التركيب:

$ cd /home/bob/Work/smarts && CLAUDE_PROJECT_DIR=$(pwd) ./bin/recording-state maybe-start
[recording] auto-started: contract-to-docs (branch: feat/contract-to-docs)

$ cat docs/notes/.state.json
{
  "feature": "contract-to-docs",
  "started_at": "2026-04-20T17:47:18-04:00",
  "branch": "feat/contract-to-docs",
  "auto_started": true
}

الـ script التقط بشكل صحيح أن smarts مصادفةً على فرع feat/contract-to-docs مع شجرة dirty — بدأ التسجيل تلقائياً، استنتج اسم الـ feature contract-to-docs من الفرع feat/contract-to-docs. المنطق في الـ script:

def branch_to_feature(branch):
    if not branch or branch in ("master", "main"):
        return None
    if "/" in branch:
        return branch.split("/", 1)[1]
    return branch

feat/XX، feature/XX، fix-y مجرد → fix-y، master/main → None (يسقط إلى اسم بطابع زمني session-YYYYMMDD-HHMM).

الإرشاد غبيٌّ عمداً بما يكفي — اسم الفرع هو موضوع العمل، لا حاجة لمطالبتك بتسمية أخرى.

حدود الرؤية عند الخلط مع Amp

بعد التركيب سألت نفسي: أعمل أحياناً في Amp — هل يُلتقط ذلك؟ الإجابة تماماً كما تتنبأ قصة طبقات الـ hooks:

السيناريو hook Claude Code hook git ما الذي يهبط في الملاحظات
عمل في Claude Code + commit ✅ يُطلَق ✅ يُطلَق تفاصيل الجلسة + هيكل الـ commit
عمل في Amp + commit ❌ بلا تأثير ✅ يُطلَق هيكل الـ commit فقط
كتابة يدوية + commit ❌ بلا تأثير ✅ يُطلَق هيكل الـ commit فقط
عمل في Claude Code، بلا commit بعد ✅ يبدأ التسجيل تفاصيل الجلسة (إدخال الـ commit ينتظر الـ commit التالي)

الخلاصة: يكفي إن كان Claude Code هو المحرك الأساسي. هيكل الـ commit يهبط دائماً؛ تفاصيل الجلسة تهبط فقط على مسار Claude Code. لمقالات "ماذا فُعِل" تعتمد غالباً على body الـ commit — prompts الجلسة / آثار bash إضافة محسنة، جميلة إن توفرت لكنها ليست شرطاً.

إن كنت تستخدم Amp بكثرة، Amp لديه آليته الخاصة للـ hooks (لم أخض في التفاصيل)؛ script صغير لإعادة التوجيه يُطلق recording-state maybe-start/maybe-stop سيعمل بنفس الطريقة.

قائمة التحقق

نقل session recording الخاص بـ Claude Code إلى مشروع آخر — 5 حركات:

  1. cp لـ script اثنين بـ Python إلى bin/ المشروع الهدف. بلا تعديل — الـ scripts تحترم $CLAUDE_PROJECT_DIR، قابلة للنقل بين المشاريع بحكم التصميم.
  2. أنشئ .claude/settings.local.json، hooks فقط. لا تنقل قائمة permissions — permissions حالة مشروع (تختلف من مشروع لآخر)؛ الـ hooks أنماط (متطابقة عبر المشاريع).
  3. أنشئ .git/hooks/post-commit (3 أسطر)، صدِّر CLAUDE_PROJECT_DIR=$ROOT يدوياً واستدعِ recording-state commit. هذه النقطة الوحيدة التي يُجسَّر فيها عالم git مع عالم Claude Code عبر نفس متغير البيئة.
  4. أضف docs/notes/ إلى .gitignore. الملاحظات مؤقتة + خاصة، ليست جزءاً من الـ repo.
  5. اختبار دخاني يدوي: CLAUDE_PROJECT_DIR=$(pwd) ./bin/recording-state maybe-start، تحقق من صحة الاستنتاج branch→feature. إن كانت الشجرة نظيفة، الـ script مُصمَّم لتخطي — ليس bug.

قرار التصميم الحقيقي ليس "كيف أنقله" — النقل تقريباً cp. القرار هو تقسيم منطق التسجيل إلى 4 طبقات منفصلة، كل طبقة تؤدي عملاً واحداً واضحاً:

  • scripts Python: helper بلا حالة، يحترم CLAUDE_PROJECT_DIR
  • hook Claude Code: أحداث داخل الأداة (لحم الجلسة)
  • hook git: أحداث محايدة للأداة (هيكل الـ commit)
  • gitignore: ضبط الضجيج

كل من الطبقات الأربع يمكن نقلها أو استبدالها أو تخطيها باستقلال (تريد دعم Amp؟ أضف طبقة hook لـ Amp. تغيير تنسيق الملاحظات؟ عدِّل Python. لا تريد أن يتتبع git الملاحظات؟ احذف post-commit). يستطيع Claude كتابة الكود بشكل صحيح — لكن حكم "إلى أي طبقة تنتمي هذه الوظيفة" لا يستطيع اتخاذه نيابة عنك. هذا حكم عن حدود أدواتك، وهو حكمك أنت.