מערכת ההקלטה של 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) תלוי באיזו שכבה התקנת.
כל מה ב-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 רצות במקביל, עם 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
- שניהם → להתקין בשתי השכבות
התקנת אותה תצורת הקלטה ב-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}
הסקריפטים בלי שינוי — הם משתמשים במשתנה הסביבה $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. שני הצדדים מכבדים את אותו משתנה סביבה, והסקריפט לא צריך להבין "באיזה פרויקט אני".
.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
.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
ה-export הידני של 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
}
הסקריפט זיהה נכון ש-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/X → X, feature/X → X, fix-y עירום → fix-y, master/main → None (שנופל חזרה לשם עם חותמת זמן session-YYYYMMDD-HHMM).
ההיוריסטיקה היא טיפשה במידה מכוונת — שם ה-branch הוא נושא העבודה, אין צורך להכריח אותך לתת שם שוב.
אחרי ההתקנה שאלתי את עצמי: אני עובד לפעמים ב-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 מהלכים:
cp שני סקריפטי Python ל-bin/ של פרויקט היעד. בלי שינוי — הסקריפטים מכבדים את $CLAUDE_PROJECT_DIR, ניתנים להעברה בין פרויקטים בעיצוב..claude/settings.local.json, hooks בלבד. אל תעביר את רשימת ה-permissions — permissions הם מצב פרויקט (שונים לכל פרויקט); hooks הם תבניות (זהים בין פרויקטים)..git/hooks/post-commit (3 שורות), ידנית export 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 נכונה. אם tree נקי, הסקריפט מתוכנן לדלג — לא באג.ההחלטה העיצובית האמיתית היא לא "איך אני מעביר" — העברה היא כמעט cp. היא חיתוך לוגיקת ההקלטה ל-4 שכבות שונות, שכל אחת עושה דבר ברור אחד:
CLAUDE_PROJECT_DIRכל אחת מ-4 השכבות ניתנת להעברה, להחלפה או לדילוג באופן עצמאי (רוצה תמיכה ב-Amp? הוסף שכבת hook של Amp. לשנות פורמט הערות? לערוך Python. לא רוצה ש-git יעקוב אחרי הערות? מחק את post-commit). Claude יכול לכתוב את הקוד נכון — אבל השיקול "לאיזו שכבה שייכת הפונקציונליות הזו" לא יכול לקבל עבורך. זה שיקול על גבולות הכלים שלך, והוא שלך.