أصعب ما في مقالات الحالة الحقيقية جمع المادة: git log فيه commit message فقط، وتفاصيل الجلسة تتبخر. أربعة hooks مع اثنين slash command تحوّل كل جلسة إلى raw.md — كلفة حدية صفر.
الجزء الأصعب في كتابة مقال "Claude فعل X من أجلي" ليس الكتابة — بل جمع المادة. git log لا يحتوي إلا على رسائل الـ commit؛ المقترحات التي رفضتها، المحاولات الفاشلة، الثلاث مراجعات لذلك الـ prompt، مخرجات الخطأ — ذلك ما يجعل المقال يستحق القراءة، وقد ضاع كله.
أسوأ: بحلول الوقت الذي تكتب فيه المقال، لم تعد تتذكر لماذا اخترت A بدلاً من B. "أذكر أنه كان هناك سبب" مقابل "ذلك السبب كان X" هو الفرق بين مقال مقنع وحشو.
أخذ محاضر الاجتماع يكلف أكثر من كتابة المقال نفسه، لذا لا تأخذها. الحل الوحيد هو ترك التسجيل يحدث من تلقاء نفسه — hooks.
جعلت Claude يكتب 4 hooks و2 slash commands تحوّل كل جلسة تطوير إلى docs/notes/<feature>/raw.md. المقال السابق ترك Claude ينشر إلى الإنتاج استمد معظم commits وقصاصات bash ومراجع الأخطاء من هذا الـ pipeline. وهذا المقال أيضاً.
4 hooks و2 slash commands يدوية تشكل شريطاً واحداً:
| المُشغِّل | ماذا يفعل |
|---|---|
| PostToolUse(Edit\ | Write\ |
| PostToolUse(Bash) | يتوقف عند العودة إلى master + نظيف |
| git post-commit | يكتب checkpoint لكل commit |
| Stop | يحلل الـ transcript عند نهاية الجلسة |
/record-feature NAME /stop-recording |
تجاوز يدوي |
ملف حالة واحد: docs/notes/.state.json. كل hook يقرأ ويكتب فيه. لا تنسيق آخر.
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/bin/recording-state maybe-start"
}]
}
]
نواة maybe-start:
def cmd_maybe_start():
if load_state(): return # التسجيل جارٍ بالفعل
if not tree_is_dirty(): return # لم يتغير شيء، تخطَّ
feature = branch_to_feature(current_branch())
if not feature:
feature = "session-" + datetime.now().strftime("%Y%m%d-%H%M")
save_state({"feature": feature, "auto_started": True, ...})
branch_to_feature("feature/pro") يُرجع "pro". المطورون يقسمون العمل أصلاً حسب الـ branch، لذا نعيد استخدام هذا الحد الذهني — السبب الجذري لبقاء هذا الـ pipeline هو أنه لا يطلب من المستخدم تذكّر شيء جديد. على master، السكربتات اللمرية تقع على timestamp.
لماذا matcher هو Edit/Write/MultiEdit: كتابة الكود هي الإشارة الحقيقية لـ "أنا أفعل شيئاً فعلاً". قراءة الملفات، تشغيل الاختبارات، طرح الأسئلة — لا تُحسب.
{
"matcher": "Bash",
"hooks": [{ "command": "$CLAUDE_PROJECT_DIR/bin/recording-state maybe-stop" }]
}
def cmd_maybe_stop():
state = load_state()
if not state or not state.get("auto_started"): return
if current_branch() not in ("master", "main"): return
if tree_is_dirty(): return
clear_state()
ثلاث بوابات: التسجيلات اليدوية لا تتوقف، فروع feature لا تتوقف، الأشجار المتسخة لا تتوقف.
لماذا التعليق على Bash بدلاً من Edit: بعد الدمج إلى master عادة لا تعدّل أكثر، لكنك بالتأكيد ستشغّل git status / git log / bin/rails test — أي أمر يمنح الـ hook فرصة ليلاحظ "وقت الختام".
علم auto_started حاسم. إن أطلقت /record-feature pro-launch وعبرت عدة branches عبر عدة merges، ستقتل قواعد auto-stop التسجيل في منتصف الميزة. التسجيلات اليدوية لا تتوقف تلقائياً أبداً — فقط /stop-recording.
ليس Claude Code hook — هذا .git/hooks/post-commit:
#!/bin/bash
ROOT=$(git rev-parse --show-toplevel) || exit 0
CLAUDE_PROJECT_DIR="$ROOT" "$ROOT/bin/recording-state" commit || true
recording-state commit يُلحق رسالة الـ commit الكاملة بـ raw.md:
### Commit 2026-04-16 21:55: `71f38a1`
> Add pricing page and expand account UI (P6 phases 1-2)
>
> Pricing (/pricing):
> - Displays all 6 plans in monthly/yearly grid with Stimulus toggle
> - Anonymous users see Subscribe → sign-in flow
> ...
لماذا رسائل commit مهمة: إنها الملخص المكتوب يدوياً الذي أنتجته أنت (أو Claude) في اللحظة التي كانت الميزة فيها منتهية للتو وكان السياق مكتملاً. أدق من التذكّر لاحقاً، أقصر من الـ transcript، أكثر تجريداً من الـ diff.
حين يكتب Claude رسالة commit، فإنه جوهرياً يكتب مادة ملخص لمقالك المستقبلي — تحتاج فقط أن يكتبها جيداً من أول مرة، لا إعادة الكتابة لاحقاً.
"Stop": [{
"hooks": [{ "command": "$CLAUDE_PROJECT_DIR/bin/extract-session-notes" }]
}]
عند انتهاء الجلسة، يمرّر Claude Code transcript_path إلى الـ hook عبر stdin كـ JSON. extract-session-notes يفتح ذلك jsonl، يحلله سطراً سطراً، ويصنّف:
keep_patterns = (
"test", "spec", "rspec", "minitest",
"kamal", "git commit", "git push",
"rails db", "rails routes", "rails runner",
"migrate", "curl -X", "curl -s -X",
)
if any(kw in cmd for kw in keep_patterns):
bash_cmds.append({"cmd": cmd[:400], "desc": desc})
أوامر bash المُدرجة في القائمة البيضاء فقط تُلتقط. 90% من bash في جلسة كتابة كود هو ls / cat / grep / head — لا تستخدمها المقالات، تُفلتر كلها. ما يتبقى (تشغيل اختبارات، kamal، rails runner، curl على APIs) هو الأوامر التي لها قصة.
prompts المستخدم المغلّفة في <command-*> أو <system-*> تُستبعد — تبقى فقط المدخلات الحقيقية. مسارات Edit/Write تُزال تكراراتها في set. مخرجات الخطأ تُقصّ عند 400 حرف. استدعاءات Task sub-agent تُحفظ (الـ prompt يُقصّ عند 2000 حرف، يكفي لرؤية ما فعله sub-agent).
الـ Stop hook لا يُطلق مرة واحدة لكل تسجيل — يُطلق كل مرة تنتهي فيها جلسة. على نفس feature branch قد تفتح وتغلق Claude Code خمس أو ست مرات. إذا أعاد كل Stop تحليل الـ transcript كاملاً وكتب كل شيء، لغرق raw.md في التكرارات.
الحل: ضع cursor last_extracted_at في ملف state:
filter_after = state.get("last_extracted_at") or state.get("started_at")
events = extract_events(transcript_path, filter_after)
# ...بعد الكتابة...
state["last_extracted_at"] = datetime.now().astimezone().isoformat()
save_state(project_dir, state)
كل مرور يلتقط فقط الأحداث بعد الـ cursor. بسيط، لكن إن نسيته ستظهر مئات الأسطر المكررة.
/record-feature NAME:
{
"feature": "NAME",
"started_at": "ISO timestamp",
"branch": "current branch",
"auto_started": false
}
سطر auto_started: false يعطّل auto-stop. حالات الاستخدام: كتابة سكربت لمرة واحدة على master مع الرغبة بترك أثر، ميزة تمتد عبر branches عدة، أو التصريح صراحة بأن "هذه للمقال".
/stop-recording: يُشغّل استخراجاً نهائياً على أحدث jsonl، ثم يمسح ملف state.
مقطع حقيقي (من docs/notes/pro/raw.md):
## Session 2026-04-16 21:52 (`7a81bf9d`)
### User prompts
- 根据环境变量直接写好对应只,不用传。
### Files edited/written
- `config/deploy.yml`
- `config/initializers/x402.rb`
### Commit 2026-04-17 00:13: `f87ea8e`
> Add production credentials (Stripe live + x402 mainnet)
> ...
### Commit 2026-04-17 00:57: `eba9ac9`
> Fix production x402 wallet_address (stray fullwidth '?' at end)
عند الكتابة: فقط grep. "أي commit كان إصلاح bug عنوان المحفظة؟" → eba9ac9. "كيف وجّهت prompt إلى sub-agent الـ refactor؟" → تحت ### Sub-agent invocations.
docs/notes/ في .gitignore — هي مادة مسوّدة لكتابتك، ليست شيفرة مصدرية.
1. الـ branch كحدّ للميزة، لا نظام metadata منفصل. المطورون يقسّمون العمل أصلاً حسب الـ branch؛ إضافة طبقة "اسم ميزة" ستنحرف حتماً. أعد استخدام الحد القائم، عبء معرفي صفر.
2. قائمة بيضاء للـ bash، لا قائمة سوداء. 95% من bash في الـ transcripts ضجيج. قائمة "ما يستحق الحفظ" تبقى مستقرة لسنوات؛ قائمة "ما يُفلتر" لا.
3. الـ cursor يعيش في ملف state، لا في الـ transcript. الـ transcript ملك لـ Claude Code؛ state ملك للـ pipeline. مفكوك الارتباط — Claude Code يمكنه تغيير صيغة الـ transcript دون كسري، ويمكنني إعادة كتابة منطق الاستخراج دون لمس الـ transcripts.
كتابة مقال 1500 كلمة تستهلك مادة من حوالي 30 commit، 4 جلسات، دزينة من أوامر bash رئيسية. جمعها يدوياً يستهلك ساعة تقريباً — يكفي لدفعك لاختصار الطريق المرة القادمة.
دع الـ hooks تفعل ذلك: كلفة حدية صفر. ثبّت هذا مرة واعمل بشكل عادي فقط — اسحب branch، اكتب كود، انشر — وتتراكم مادة المقال لوحدها.
كل مرجع commit، قصاصة bash، سطر خطأ، ومسار ملف في هذا المقال جاء من docs/notes/pro/raw.md — مادة مسوّدة سجّلها الـ hook بنفسه منذ اللحظة التي سحبت فيها feature/pro قبل يومين. حين حان وقت الكتابة، فتحت الملف وكان كل ما أحتاجه هناك.
أفضل طريقة لجعل Claude يكتب مقالات عن Claude Code هي أولاً جعل Claude يكتب hooks ليسجّل نفسه.