Najtrudniejsze w artykułach studium przypadku to zebranie materiału: git log ma tylko commit messages, szczegóły sesji znikają. Cztery hooki i dwa slash commands zamieniają każdą sesję w raw.md — koszt krańcowy zero.
Najtrudniejszą częścią pisania artykułu "Claude zrobił dla mnie X" nie jest pisanie — to zbieranie materiału. W git logu są tylko commit messages; odrzucone propozycje, nieudane próby, trzy przeróbki tamtego prompta, wyjście błędu — to właśnie czyni artykuł wartym czytania i wszystko to zniknęło.
Gorzej: gdy siadasz do pisania, już nie pamiętasz, dlaczego wybrałeś A zamiast B. "Chyba miałem powód" vs "ten powód to X" to różnica między wiarygodnym artykułem a zapychaczem.
Protokoły ze spotkań kosztują więcej niż sam artykuł, więc ich nie robisz. Jedyne rozwiązanie to pozwolić, żeby nagrywanie działo się samo — hooki.
Kazałem Claude'owi napisać 4 hooki + 2 slash commands, które zamieniają każdą sesję dev w docs/notes/<feature>/raw.md. Poprzedni artykuł Niech Claude wdraża na produkcję wyciągnął większość cytowanych commitów, fragmentów bash i referencji do błędów z tego pipeline'u. Ten też.
4 hooki + 2 ręczne slash commands tworzą jedną taśmę:
| Trigger | Co robi |
|---|---|
| PostToolUse(Edit\ | Write\ |
| PostToolUse(Bash) | Stop, gdy wracasz na master + czysto |
| git post-commit | Pisze checkpoint przy każdym commicie |
| Stop | Parsuje transcript na końcu sesji |
/record-feature NAME /stop-recording |
Ręczne nadpisanie |
Jeden plik stanu: docs/notes/.state.json. Wszystkie hooki czytają i piszą do niego. Żadnej innej koordynacji.
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/bin/recording-state maybe-start"
}]
}
]
Rdzeń maybe-start:
def cmd_maybe_start():
if load_state(): return # już nagrywamy
if not tree_is_dirty(): return # nic się nie zmieniło, pomiń
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") zwraca "pro". Programiści i tak dzielą pracę po branchach, więc reużywamy tę mentalną granicę — głębszy powód, dla którego ten pipeline przeżywa: nie prosi użytkownika, by pamiętał nic nowego. Na masterze jednorazowe skrypty spadają na timestamp.
Dlaczego matcher to Edit/Write/MultiEdit: pisanie kodu to prawdziwy sygnał "faktycznie coś robię". Czytanie plików, odpalanie testów, zadawanie pytań — nie liczy się.
{
"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()
Trzy bramki: nagrania ręczne się nie zatrzymują, feature branches się nie zatrzymują, brudne drzewo się nie zatrzymuje.
Dlaczego zahaczony na Bash a nie na Edit: po merge'u na master zazwyczaj już nie edytujesz, ale na pewno uruchomisz git status / git log / bin/rails test — jakakolwiek komenda daje hookowi szansę zauważyć "czas zamykać".
Flaga auto_started jest nośna. Jeśli odpalisz /record-feature pro-launch i przejdziesz przez wiele branchów przez wiele merge'ów, reguły auto-stopu zabiłyby nagranie w środku feature'a. Nagrania ręczne nigdy się nie zatrzymują automatycznie — tylko /stop-recording.
To nie hook Claude Code — to .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 dopisuje pełną wiadomość commita do 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
> ...
Dlaczego commit messages się liczą: to ręcznie napisane streszczenie, które ty (albo Claude) wyprodukował dokładnie w momencie, gdy feature właśnie się skończył, a kontekst był pełny. Dokładniejsze niż późniejsze wspomnienia, krótsze niż transcript, bardziej abstrakcyjne niż diff.
Gdy Claude pisze commit message, w zasadzie pisze materiał-streszczenie do twojego przyszłego artykułu — potrzebujesz tylko, by zrobił to dobrze za pierwszym razem, bez przepisywania.
"Stop": [{
"hooks": [{ "command": "$CLAUDE_PROJECT_DIR/bin/extract-session-notes" }]
}]
Gdy sesja się kończy, Claude Code przekazuje transcript_path do hooka przez stdin JSON. extract-session-notes otwiera ten jsonl, idzie po nim linia po linii i sortuje:
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})
Łapane są tylko komendy bash z whitelisty. 90% basha w sesji kodowej to ls / cat / grep / head — artykuły tego nie używają, wszystko odfiltrowane. To, co zostaje (testy, kamal, rails runner, curl do API), to komendy z historią za nimi.
Prompty użytkownika owinięte w <command-*> albo <system-*> są odrzucane — przeżywa tylko prawdziwy input. Ścieżki Edit/Write deduplikowane w set. Wyjście błędu obcięte do 400 znaków. Wywołania Task sub-agenta są zachowywane (prompt obcięty do 2000 znaków, starczy, by zobaczyć, co sub-agent zrobił).
Hook Stop nie strzela raz na nagranie — strzela za każdym razem, gdy sesja się kończy. Na tej samej feature branch możesz otworzyć i zamknąć Claude Code pięć-sześć razy. Gdyby każdy Stop re-parsował cały transcript i pisał wszystko, raw.md utonąłby w duplikatach.
Rozwiązanie: kursor last_extracted_at w pliku stanu:
filter_after = state.get("last_extracted_at") or state.get("started_at")
events = extract_events(transcript_path, filter_after)
# ...po zapisie...
state["last_extracted_at"] = datetime.now().astimezone().isoformat()
save_state(project_dir, state)
Każdy przebieg bierze tylko eventy po kursorze. Proste, ale zapomnij — i sypie setki zduplikowanych linii.
/record-feature NAME:
{
"feature": "NAME",
"started_at": "ISO timestamp",
"branch": "current branch",
"auto_started": false
}
Linia auto_started: false wyłącza auto-stop. Przypadki użycia: piszesz jednorazowy skrypt na masterze, ale chcesz zostawić ślad; feature przechodzący przez wiele branchów; wyraźnie zadeklarować "ten jest na artykuł".
/stop-recording: odpala finalną ekstrakcję na najnowszym jsonlu, potem czyści plik stanu.
Prawdziwy wycinek (z 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)
Do pisania artykułu: po prostu grep. "Który commit był fixem wallet address?" → eba9ac9. "Jak zapromptowałem sub-agenta od refactora?" → pod ### Sub-agent invocations.
docs/notes/ jest w .gitignore — to materiał roboczy do pisania, nie kod źródłowy.
1. Branch jako granica feature'a, nie osobny system metadanych. Programiści i tak dzielą pracę po branchach; dorzucanie kolejnej warstwy "nazwa feature'a" nieuchronnie rozjedzie się. Reużycie istniejącej granicy, zero dodatkowego obciążenia poznawczego.
2. Whitelista dla basha, nie blacklista. 95% basha w transcriptach to szum. Lista "co warto zachować" jest stabilna przez lata; lista "co odfiltrować" — nie.
3. Kursor żyje w pliku stanu, nie w transcripcie. Transcript należy do Claude Code; state należy do pipeline. Odsprzężone — Claude Code może zmieniać format transcriptu bez psucia mnie, a ja mogę przepisać logikę ekstrakcji bez ruszania transcriptów.
Pisanie artykułu na 1500 słów pochłania materiał z około 30 commitów, 4 sesji, tuzina kluczowych komend bash. Zebranie ręcznie kosztuje jakąś godzinę — wystarczająco, by następnym razem ścinać zakręty.
Niech robią to hooki: koszt krańcowy zero. Zainstaluj to raz i po prostu pracuj normalnie — pobierz brancha, pisz kod, wdrażaj — a materiał do artykułu układa się sam.
Każdy odnośnik do commita, snippet bash, linia błędu i ścieżka pliku w tym artykule pochodzą z docs/notes/pro/raw.md — materiału roboczego, który hook sam zapisał od chwili, gdy dwa dni temu pobrałem feature/pro. Gdy przyszło do pisania, otworzyłem plik i wszystko, czego potrzebowałem, tam było.
Najlepszy sposób, by Claude pisał artykuły o Claude Code, to najpierw kazać Claude'owi napisać hooki, które go samego nagrywają.