Free

לתת ל-Claude לכתוב hooks שמקליטים אותו עצמו

הכי קשה במאמרי מקרה אמיתי זה לאסוף חומר: git log מחזיק רק commit messages, פרטי הסשן נעלמים. ארבעה hooks ושני slash commands הופכים כל סשן ל-raw.md — עלות שולית אפס.


החלק הכי קשה בכתיבת מאמר "Claude עשה עבורי X" הוא לא הכתיבה — זה איסוף החומר. git log מחזיק רק commit messages; הגישות שדחית, הניסיונות הכושלים, שלוש הגרסאות של אותו prompt, פלט השגיאה — זה מה שהופך מאמר לשווה קריאה, והכל אבד.

גרוע יותר: כשאתה כבר כותב, אתה לא זוכר למה בחרת ב-A ולא ב-B. "נדמה לי שהייתה סיבה" מול "אותה סיבה הייתה X" — זה ההבדל בין מאמר אמין למילוי מקום.

רישום פרוטוקולים עולה יותר מכתיבת המאמר, אז אתה לא רושם. הפתרון היחיד: שההקלטה תקרה לבד — hooks.

גרמתי ל-Claude לכתוב 4 hooks ו-2 slash commands שהופכים כל session פיתוח ל-docs/notes/<feature>/raw.md. המאמר הקודם לתת ל-Claude לפרוס ל-production שאב את רוב ה-commits, שברי bash והפניות לשגיאות מתוך הצינור הזה. גם המאמר הזה.


סקירת הצינור

4 hooks ושני slash commands ידניים יוצרים מסוע אחד:

טריגר מה עושה
PostToolUse(Edit\ Write\
PostToolUse(Bash) עוצר כשחוזרים ל-master + נקי
git post-commit כותב checkpoint לכל commit
Stop מפרסר transcript בסוף הסשן
/record-feature NAME /stop-recording עקיפה ידנית

קובץ state יחיד: docs/notes/.state.json. כל hook קורא וכותב אליו. אין עוד תיאום.

Hook #1: מתי להתחיל

"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, אז ממחזרים את אותו גבול מנטלי — הסיבה השורשית לכך שהצינור שורד: הוא לא מבקש מהמשתמש לזכור שום דבר חדש. על master, סקריפטים חד-פעמיים נופלים ל-timestamp.

למה ה-matcher הוא Edit/Write/MultiEdit: כתיבת קוד היא הסימן האמיתי של "אני באמת עושה משהו". קריאת קבצים, הרצת בדיקות, שאילת שאלות — לא נחשב.

Hook #2: מתי לעצור

{
  "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 branches לא עוצרות, עץ מלוכלך לא עוצר.

למה מקושר ל-Bash ולא ל-Edit: אחרי merge ל-master בדרך כלל כבר לא עורכים, אבל בטוח ירוצו git status / git log / bin/rails test — כל פקודה נותנת ל-hook הזדמנות להבחין ב-"זמן לסגור".

דגל auto_started הוא קריטי. אם תריץ /record-feature pro-launch ידנית ותעבור דרך מספר branches על פני מספר merges, כללי auto-stop יהרגו את ההקלטה באמצע feature. הקלטות ידניות לעולם לא נעצרות אוטומטית — רק /stop-recording.

Hook #3: commit כ-checkpoint

לא hook של Claude Code — זה .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) הפקת בדיוק ברגע שה-feature זה עתה הסתיים וההקשר היה מלא. מדויק יותר מלזכור אחר כך, קצר יותר מה-transcript, מופשט יותר מה-diff.

כש-Claude כותב הודעת commit, הוא בעצם כותב חומר סיכום למאמר העתידי שלך — אתה רק צריך שהוא יעשה את זה טוב בפעם הראשונה, בלי שכתוב אחר כך.

Hook #4: חילוץ אירועים מה-transcript

"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 מה-whitelist נשמרות. 90% מה-bash בסשן קוד הוא ls / cat / grep / head — מאמרים לא משתמשים בזה, הכל מסונן. מה שנשאר (הרצת בדיקות, kamal, rails runner, curl מול APIs) אלו הפקודות עם סיפור מאחור.

prompts של משתמש עטופים ב-<command-*> או <system-*> נזרקים — רק הקלט האמיתי שורד. נתיבי Edit/Write מעובדים לסט ללא כפילויות. פלט שגיאות חתוך ל-400 תווים. קריאות Task sub-agent נשמרות (prompt חתוך ל-2000 תווים, מספיק לראות מה ה-sub-agent עשה).

הקלטה אחת, Stop-ים רבים: השתמש ב-cursor

ה-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. פשוט, אבל תשכח — ותקבל מאות שורות כפולות.

עקיפה ידנית: שני slash commands

/record-feature NAME:

{
  "feature": "NAME",
  "started_at": "ISO timestamp",
  "branch": "current branch",
  "auto_started": false
}

השורה auto_started: false מבטלת את auto-stop. מקרי שימוש: לכתוב סקריפט חד-פעמי על master אבל עדיין להשאיר עקבות; feature שחוצה מספר branches; להצהיר במפורש "זה למאמר".

/stop-recording: מריץ extraction סופי מול ה-jsonl העדכני ביותר, ואז מנקה את קובץ ה-state.

איך נראה raw.md

קטע אמיתי (מתוך 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 היה fix של wallet address?" → eba9ac9. "איך prompt-תי את sub-agent ה-refactor?" → תחת ### Sub-agent invocations.

docs/notes/ ב-.gitignore — זה חומר טיוטה לכתיבה שלך, לא קוד מקור.

שלוש החלטות עיצוב לא מובנות מאליהן

1. Branch כגבול של feature, לא מערכת metadata נפרדת. מפתחים כבר מחלקים עבודה לפי branch; הוספת שכבה נוספת של "שם feature" תסטה בהכרח. ממחזרים את הגבול הקיים, אפס עומס קוגניטיבי.

2. Whitelist ל-bash, לא blacklist. 95% מה-bash ב-transcripts הוא רעש. שמירה על רשימת "מה שווה לשמור" יציבה שנים; שמירה על רשימת "מה לסנן" לא.

3. ה-cursor חי בקובץ ה-state, לא ב-transcript. ה-transcript שייך ל-Claude Code; ה-state שייך ל-pipeline. מפוצלים — Claude Code יכול לשנות פורמט transcript בלי לשבור אותי, ואני יכול לשכתב את לוגיקת החילוץ בלי לגעת ב-transcripts.

סוגרים את המעגל

כתיבת מאמר של 1500 מילים צורכת חומר של בערך 30 commits, 4 סשנים, תריסר פקודות bash מפתח. לאסוף ביד עולה בערך שעה — מספיק כדי שבפעם הבאה תחתוך פינות.

תן ל-hooks לעשות את זה: עלות שולית אפס. התקן את זה פעם אחת ופשוט עבוד רגיל — משוך branch, כתוב קוד, פרוס — וחומר המאמר מצטבר לבד.

כל הפניה ל-commit, קטע bash, שורת שגיאה ונתיב קובץ במאמר הזה הגיעו מ-docs/notes/pro/raw.md — חומר טיוטה ש-hook הקליט לבד מהרגע שמשכתי feature/pro לפני יומיים. כשהגיע הזמן לכתוב, פתחתי את הקובץ והכל מה שהייתי צריך היה שם.

הדרך הכי טובה לגרום ל-Claude לכתוב מאמרים על Claude Code היא קודם לגרום ל-Claude לכתוב hooks שמקליטים אותו עצמו.