نقل نظام تسجيل 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) يعتمد على الطبقة التي تُركِّب فيها.
كل ما في 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 الخاصة بـ 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
- الاثنان معاً → ركِّب في الطبقتين
تركيب نفس إعدادات التسجيل في smarts (/home/bob/Work/smarts، مشروع Rails).
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 إلى أن يحدد "في أي مشروع أنا".
.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
.git/hooks/post-commit3 أسطر:
#!/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
docs/notes/ إلى .gitignore# Session recording notes (transient, for article material)
docs/notes/
الملاحظات مؤقتة + خاصة — لا تريد أن يُرفع raw.md ضمن الـ PRs؛ لا تريد أن يُلوِّث .state.json ناتج git status. gitignore الخاص بـ how2claude يعاملها بالطريقة نفسها.
أطلق 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/X → X، feature/X → X، fix-y مجرد → fix-y، master/main → None (يسقط إلى اسم بطابع زمني session-YYYYMMDD-HHMM).
الإرشاد غبيٌّ عمداً بما يكفي — اسم الفرع هو موضوع العمل، لا حاجة لمطالبتك بتسمية أخرى.
بعد التركيب سألت نفسي: أعمل أحياناً في 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 حركات:
cp لـ script اثنين بـ Python إلى bin/ المشروع الهدف. بلا تعديل — الـ scripts تحترم $CLAUDE_PROJECT_DIR، قابلة للنقل بين المشاريع بحكم التصميم..claude/settings.local.json، hooks فقط. لا تنقل قائمة permissions — permissions حالة مشروع (تختلف من مشروع لآخر)؛ الـ hooks أنماط (متطابقة عبر المشاريع)..git/hooks/post-commit (3 أسطر)، صدِّر CLAUDE_PROJECT_DIR=$ROOT يدوياً واستدعِ recording-state commit. هذه النقطة الوحيدة التي يُجسَّر فيها عالم git مع عالم Claude Code عبر نفس متغير البيئة.docs/notes/ إلى .gitignore. الملاحظات مؤقتة + خاصة، ليست جزءاً من الـ repo.CLAUDE_PROJECT_DIR=$(pwd) ./bin/recording-state maybe-start، تحقق من صحة الاستنتاج branch→feature. إن كانت الشجرة نظيفة، الـ script مُصمَّم لتخطي — ليس bug.قرار التصميم الحقيقي ليس "كيف أنقله" — النقل تقريباً cp. القرار هو تقسيم منطق التسجيل إلى 4 طبقات منفصلة، كل طبقة تؤدي عملاً واحداً واضحاً:
CLAUDE_PROJECT_DIRكل من الطبقات الأربع يمكن نقلها أو استبدالها أو تخطيها باستقلال (تريد دعم Amp؟ أضف طبقة hook لـ Amp. تغيير تنسيق الملاحظات؟ عدِّل Python. لا تريد أن يتتبع git الملاحظات؟ احذف post-commit). يستطيع Claude كتابة الكود بشكل صحيح — لكن حكم "إلى أي طبقة تنتمي هذه الوظيفة" لا يستطيع اتخاذه نيابة عنك. هذا حكم عن حدود أدواتك، وهو حكمك أنت.