System nagrywania how2claude do innego projektu Rails: 4 pliki, 5 kroków, warstwowanie hooków.
how2claude ma zestaw hooków, które automatycznie nagrywają sesje Claude Code — startują nagrywanie przy rozpoczęciu pracy, dodają checkpoint przy każdym commicie, a po zakończeniu sesji wyciągają prompts / bash / listę edycji do docs/notes/<feature>/raw.md. Artykuł let-claude-record-itself pokazał, jak to zostało zbudowane.
Problem: mój drugi projekt (smarts, strona docs dla smart kontraktów) nie miał tego wcale. Za każdym razem, gdy chciałem napisać artykuł po fakcie, grzebałem w git log i pamięci, z poczuciem, że umyka mi to najlepsze. Ten artykuł opowiada o przeniesieniu systemu nagrywania tam — w sumie 4 pliki, 5 minut — ale po drodze wypłynęła prawdziwa intuicja o warstwowaniu hooków: hooki Claude Code i hooki gita działają na zupełnie różnych warstwach, a co zostanie uchwycone przy mieszaniu narzędzi (np. Amp + Claude Code) zależy od warstwy, na której zainstalujesz.
Wszystko w how2claude związane z nagrywaniem:
| Plik | Warstwa | Rola |
|---|---|---|
bin/recording-state |
skrypt | Python helper, zarządza cyklem życia .state.json |
bin/extract-session-notes |
skrypt | Python helper, czyta transcript Claude Code → pisze do raw.md |
.claude/settings.local.json |
hook Claude Code | PostToolUse / Stop triggerują powyższe skrypty |
.git/hooks/post-commit |
hook git | każdy commit woła recording-state commit po checkpoint |
.gitignore |
kontrola szumu | trzyma docs/notes/ poza repo (notatki są prywatne/ulotne) |
4 elementy, 4 różne warstwy. To rozróżnienie wraca niżej.
Dwa systemy hooków działają jednocześnie, ale o zupełnie różnych zasięgach:
Hooki Claude Code (zdefiniowane w .claude/settings.local.json):
- Zasięg: odpalają się tylko wewnątrz narzędzia Claude Code
- Triggery: PostToolUse / Stop / PreToolUse — zdarzenia cyklu życia Claude Code
- Dostępne info: nazwa tool, argumenty, transcript_path (pełny jsonl sesji) — rzeczy znane tylko Claude Code
Hooki gita (skrypty shell pod .git/hooks/):
- Zasięg: odpalają się na każdym zdarzeniu gita, niezależnie od tego, kto go pociągnął
- Triggery: post-commit / pre-push / itd.
- Dostępne info: to, co sam git wie (sha, autor, branch, diff)
Realny skutek: piszesz kod + commit wewnątrz Claude Code — odpalają się obie warstwy, info sesji i info commita lądują w raw.md. Przełączasz się na Amp (lub Cursor, albo pisanie ręczne) do pisania + commita i odpala się tylko hook gita — raw.md dostaje szkielet commita, ale zero promptów / bash / szczegółów edycji sesji.
To nie bug — to ograniczenie projektowe każdego narzędzia. Chcenie szczegółu poziomu sesji pod każdym narzędziem znaczy instalację własnej warstwy hooka na narzędzie. Siatka bezpieczeństwa gita daje ci „co zostało zrobione"; nie daje „co zostało pomyślane, gdzie się zepsuło".
Przewodnik wyboru:
- Szkielet agnostyczny względem narzędzia (info commita, zmiany kodu) → do hooka gita
- Mięso specyficzne dla Claude Code (pełne prompty, rozumowanie) → do hooka Claude Code
- Oba → instaluj na obu warstwach
Instalacja tego samego setupu nagrywania w smarts (/home/bob/Work/smarts, projekt 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}
Skrypty bez zmian — używają zmiennej środowiskowej $CLAUDE_PROJECT_DIR, żeby zdecydować, gdzie pisać:
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"
To kluczowa abstrakcja: Claude Code ustawia CLAUDE_PROJECT_DIR automatycznie przy odpalaniu hooków; my ustawiamy ją ręcznie wewnątrz git post-commit. Obie strony honorują tę samą env var, a skrypt nie musi domyślać się „w którym jestem projekcie".
.claude/settings.local.jsonJedna decyzja tutaj: przenosimy tylko hooki, nie listę permissions.
settings.local.json how2claude ma ponad 100 wpisów permissions.allow — wszystkie specyficzne dla how2claude (curl localhost:3000, bin/rails runner, kamal app exec). Zero sensu ciągnąć je do smarts. smarts sam organicznie zgromadzi swoje permissions w trakcie używania.
Hooki to wzorce — identyczne między projektami. Permissions to stan projektu — różne między projektami.
{
"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" }
]
}
]
}
}
Trzy punkty triggera:
- Plik edytowany → próba wystartowania nagrywania (maybe-start sam się sprawdza: jeśli już nagrywa, pomija, jeśli tree clean, też pomija)
- Bash wykonany → próba zatrzymania (maybe-stop jest ścisły: zatrzymuje tylko, gdy „auto-start ORAZ wrócono na master ORAZ tree clean" są wszystkie prawdziwe razem)
- Sesja zakończona → wyciąg transcriptu do raw.md
.git/hooks/post-commit3 linie:
#!/bin/bash
ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
CLAUDE_PROJECT_DIR="$ROOT" "$ROOT/bin/recording-state" commit || true
Ręczny export CLAUDE_PROJECT_DIR=$ROOT to dokładny punkt, w którym świat gita i świat Claude Code są mostkowane przez tę samą env var. || true gwarantuje, że hook nigdy nie blokuje commita.
chmod +x /home/bob/Work/smarts/.git/hooks/post-commit
docs/notes/ do .gitignore# Session recording notes (transient, for article material)
docs/notes/
Notatki są ulotne + prywatne — nie chcesz, by raw.md został commitnięty w PR; nie chcesz, by .state.json zaśmiecał git status. Gitignore how2claude traktuje to tak samo.
Zaraz po instalacji ręcznie odpalić 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
}
Skrypt prawidłowo zauważył, że smarts akurat jest na feat/contract-to-docs z brudnym tree — automatycznie wystartował nagrywanie, wyprowadził nazwę feature contract-to-docs z brancha feat/contract-to-docs. Ta logika w skrypcie:
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, goły fix-y → fix-y, master/main → None (co spada na nazwę z timestampem session-YYYYMMDD-HHMM).
Heurystyka jest celowo tępa w sam raz — nazwa brancha jest tematem pracy, nie trzeba zmuszać cię do ponownego nazywania.
Po instalacji zapytałem siebie: czasem pracuję w Ampie — czy to się łapie? Odpowiedź dokładnie taka, jak przewiduje historia warstwowania hooków:
| Scenariusz | Hook Claude Code | Hook git | Co trafia do notatek |
|---|---|---|---|
| Praca Claude Code + commit | ✅ odpala | ✅ odpala | szczegół sesji + szkielet commita |
| Praca Amp + commit | ❌ no-op | ✅ odpala | tylko szkielet commita |
| Pisanie ręczne + commit | ❌ no-op | ✅ odpala | tylko szkielet commita |
| Praca Claude Code, bez commita jeszcze | ✅ startuje nagrywanie | — | szczegół sesji (wpis commita czeka na kolejny commit) |
Wniosek: wystarczy, jeśli Claude Code jest głównym kierowcą. Szkielet commita trafia zawsze; szczegół sesji trafia tylko na trasie Claude Code. Do artykułów „co zostało zrobione" opierasz się głównie na body commita — prompty / ślady bash sesji to bonus, fajnie mieć, ale nie wymagane.
Jeśli używasz Ampa intensywnie, Amp ma własny system hooków (nie zagłębiałem się w szczegóły); mały skrypt-przekaźnik odpalający recording-state maybe-start/maybe-stop zadziała tak samo.
Przeniesienie session recording z Claude Code do innego projektu — 5 ruchów:
cp dwa skrypty Python do bin/ projektu docelowego. Bez modyfikacji — skrypty honorują $CLAUDE_PROJECT_DIR, projektowo przenośne między projektami..claude/settings.local.json, tylko hooki. Nie przenoś listy permissions — permissions to stan projektu (różne per projekt); hooki to wzorce (identyczne między projektami)..git/hooks/post-commit (3 linie), ręcznie export CLAUDE_PROJECT_DIR=$ROOT i wywołać recording-state commit. To jedyny punkt, gdzie świat gita i świat Claude Code są mostkowane przez tę samą env var.docs/notes/ do .gitignore. Notatki są ulotne + prywatne, nie są częścią repo.CLAUDE_PROJECT_DIR=$(pwd) ./bin/recording-state maybe-start, sprawdzić, czy inferencja branch→feature jest poprawna. Jeśli tree clean, skrypt jest zaprojektowany, by pomijać — to nie bug.Prawdziwa decyzja projektowa to nie „jak przenoszę" — przeniesienie to niemal cp. To rozcięcie logiki nagrywania na 4 różne warstwy, każda robi jedną jasną rzecz:
CLAUDE_PROJECT_DIRKażdą z 4 warstw można przenieść, wymienić lub pominąć niezależnie (chcesz wsparcia dla Ampa? dodaj warstwę hooka Ampa. Zmieniasz format notatek? edytuj Python. Nie chcesz, żeby git śledził notatki? usuń post-commit). Claude może napisać kod poprawnie — ale osąd „do której warstwy należy ta funkcjonalność" nie może go zrobić za ciebie. To osąd o granicach twojego tooling'u, i jest twój.