Free

העברת session recording של Claude Code לפרויקט שני

מערכת ההקלטה של how2claude לפרויקט Rails אחר: 4 קבצים, 5 שלבים, שכבות hook.


ל-how2claude יש סט hooks שמקליטים את ה-sessions של Claude Code אוטומטית — מתחילים הקלטה כשהעבודה נפתחת, מוסיפים checkpoint בכל commit, ובסוף ה-session מחלצים prompts / bash / רשימת edits לתוך docs/notes/<feature>/raw.md. המאמר let-claude-record-itself סיפר איך בניתי את זה.

הבעיה: הפרויקט השני שלי (smarts, אתר docs לחוזים חכמים) לא היה בו שום דבר מזה. בכל פעם שרציתי לכתוב מאמר בדיעבד, הייתי חופר ב-git log ובזיכרון, עם תחושה שהחלקים הטובים פשוט חומקים. המאמר הזה מספר איך העברתי את מערכת ההקלטה לשם — בסך הכול 4 קבצים, 5 דקות — אבל בדרך צפה תובנה אמיתית על שכבות hooks: hooks של Claude Code ו-hooks של git פועלים בשכבות שונות לחלוטין, ומה נתפס כשאתה מערבב כלים (למשל Amp + Claude Code) תלוי באיזו שכבה התקנת.


4 קבצים, תמונה אחת

כל מה ב-how2claude שקשור להקלטה:

קובץ שכבה תפקיד
bin/recording-state סקריפט Python helper, מנהל את מחזור החיים של .state.json
bin/extract-session-notes סקריפט Python helper, קורא transcript של Claude Code → כותב ל-raw.md
.claude/settings.local.json hook Claude Code PostToolUse / Stop מפעילים את הסקריפטים למעלה
.git/hooks/post-commit hook git כל commit קורא ל-recording-state commit לעבור checkpoint
.gitignore בקרת רעש שומר את docs/notes/ מחוץ ל-repo (הערות הן פרטיות/ארעיות)

4 חלקים, 4 שכבות שונות. ההבחנה הזו חוזרת בהמשך.

התובנה המרכזית: שכבות hooks מחליטות מה נתפס

שתי מערכות hooks רצות במקביל, עם scope שונה לגמרי:

Hooks של Claude Code (מוגדרים ב-.claude/settings.local.json):
- Scope: נורים רק בתוך הכלי Claude Code
- מפעילים: PostToolUse / Stop / PreToolUse — אירועים של מחזור חיים Claude Code
- מידע זמין: שם tool, args, transcript_path (jsonl מלא של ה-session) — דברים שרק Claude Code יודע

Hooks של git (סקריפטי shell תחת .git/hooks/):
- Scope: נורים על כל אירוע git, בלי חשיבות מי הפעיל את git
- מפעילים: post-commit / pre-push / וכו'
- מידע זמין: מה ש-git עצמו יודע (sha, מחבר, branch, diff)

השלכה אמיתית: כתיבת קוד + commit בתוך Claude Code מפעילה את שתי השכבות — מידע session ומידע commit נוחתים ב-raw.md. מעבר ל-Amp (או Cursor, או הקלדה ידנית) לכתיבה + commit נורה רק hook של git — raw.md מקבל שלד commit אבל אף פרומפט / bash / פרט edit של session.

זה לא באג — זו אילוץ התכנון של כל כלי. לרצות פרט ברמת session תחת כל כלי אומר להתקין שכבת hook משלך לכל כלי. רשת הביטחון של git נותנת לך את "מה נעשה"; לא נותנת את "מה נחשב, איפה נשבר".

מדריך בחירה:
- שלד נטול-כלי (מידע commit, שינויי קוד) → לשים ב-hook של git
- בשר ייחודי ל-Claude Code (פרומפטים מלאים, חשיבה) → לשים ב-hook של Claude Code
- שניהם → להתקין בשתי השכבות

העברה ב-5 מהלכים

התקנת אותה תצורת הקלטה ב-smarts (/home/bob/Work/smarts, פרויקט Rails).

1. להעתיק את שני סקריפטי ה-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}

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

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 בודק את עצמו: אם כבר מקליט, דלג; אם tree נקי, דלג גם)
- פקודת bash רצה → ניסיון לעצור (maybe-stop מחמיר: עוצר רק כאשר "הופעל אוטומטית וגם חזרנו ל-master וגם tree נקי" כולם מתקיימים בו-זמנית)
- Session הסתיים → חילוץ 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

ה-export הידני של 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
}

הסקריפט זיהה נכון ש-smarts נמצא במקרה על branch feat/contract-to-docs עם tree מלוכלך — התחיל הקלטה אוטומטית, הסיק שם feature contract-to-docs מה-branch feat/contract-to-docs. הלוגיקה ההיא בסקריפט:

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).

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

גבול הנראות בערבוב עם Amp

אחרי ההתקנה שאלתי את עצמי: אני עובד לפעמים ב-Amp — האם זה נתפס? התשובה בדיוק כמו שסיפור שכבות ה-hooks חוזה:

תרחיש Hook Claude Code Hook git מה נוחת בהערות
עבודה ב-Claude Code + commit ✅ נורה ✅ נורה פרטי session + שלד commit
עבודה ב-Amp + commit ❌ no-op ✅ נורה רק שלד commit
הקלדה ידנית + commit ❌ no-op ✅ נורה רק שלד commit
עבודה ב-Claude Code, בלי commit עדיין ✅ מתחיל הקלטה פרטי session (רישום commit מחכה ל-commit הבא)

מסקנה: מספיק אם Claude Code הוא הנהג הראשי. שלד commit נוחת תמיד; פרטי session נוחתים רק על המסלול של Claude Code. למאמרי "מה נעשה" אתה נשען בעיקר על body של commit — prompts / עקבות bash של session הם בונוס, נחמד יש, אבל לא חובה.

אם אתה משתמש ב-Amp בכבדות, ל-Amp יש מנגנון hook משלו (לא התעמקתי בפרטים); סקריפט העברה קטן שמפעיל recording-state maybe-start/maybe-stop יעבוד אותו דבר.

רשימת בדיקה

העברת session recording של Claude Code לפרויקט אחר — 5 מהלכים:

  1. cp שני סקריפטי Python ל-bin/ של פרויקט היעד. בלי שינוי — הסקריפטים מכבדים את $CLAUDE_PROJECT_DIR, ניתנים להעברה בין פרויקטים בעיצוב.
  2. יצירת .claude/settings.local.json, hooks בלבד. אל תעביר את רשימת ה-permissions — permissions הם מצב פרויקט (שונים לכל פרויקט); hooks הם תבניות (זהים בין פרויקטים).
  3. יצירת .git/hooks/post-commit (3 שורות), ידנית export 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 נכונה. אם tree נקי, הסקריפט מתוכנן לדלג — לא באג.

ההחלטה העיצובית האמיתית היא לא "איך אני מעביר" — העברה היא כמעט cp. היא חיתוך לוגיקת ההקלטה ל-4 שכבות שונות, שכל אחת עושה דבר ברור אחד:

  • סקריפטי Python: helper חסר מצב, מכבד את CLAUDE_PROJECT_DIR
  • Hook Claude Code: אירועים פנים-כלי (בשר ה-session)
  • Hook git: אירועים נטולי-כלי (שלד ה-commit)
  • gitignore: בקרת רעש

כל אחת מ-4 השכבות ניתנת להעברה, להחלפה או לדילוג באופן עצמאי (רוצה תמיכה ב-Amp? הוסף שכבת hook של Amp. לשנות פורמט הערות? לערוך Python. לא רוצה ש-git יעקוב אחרי הערות? מחק את post-commit). Claude יכול לכתוב את הקוד נכון — אבל השיקול "לאיזו שכבה שייכת הפונקציונליות הזו" לא יכול לקבל עבורך. זה שיקול על גבולות הכלים שלך, והוא שלך.