Free

Niech Claude pisze hooki nagrywające siebie

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


Przegląd pipeline

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.

Hook #1: kiedy zacząć

"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ę.

Hook #2: kiedy zatrzymać

{
  "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.

Hook #3: commit jako checkpoint

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.

Hook #4: wyciągnij eventy z transcriptu

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

Jedno nagranie, wiele Stopów: użyj kursora

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.

Ręczne nadpisanie: dwa slash commands

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

Jak wygląda raw.md

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.

Trzy nieoczywiste decyzje projektowe

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.

Zamykając pętlę

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