Free

Przeniesienie session recording z Claude Code do drugiego projektu

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.


4 pliki, jeden obraz

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.

Kluczowa intuicja: warstwowanie hooków decyduje, co jest uchwycane

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

Przeniesienie w 5 ruchach

Instalacja tego samego setupu nagrywania w smarts (/home/bob/Work/smarts, projekt Rails).

1. Skopiować dwa skrypty 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}

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

2. Stworzyć .claude/settings.local.json

Jedna 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

3. Stworzyć .git/hooks/post-commit

3 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

4. Dodać 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.

5. Smoke test (z małą niespodzianką)

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/XX, feature/XX, goły fix-yfix-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.

Granica widoczności przy mieszaniu z Amp

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.

Lista kontrolna

Przeniesienie session recording z Claude Code do innego projektu — 5 ruchów:

  1. cp dwa skrypty Python do bin/ projektu docelowego. Bez modyfikacji — skrypty honorują $CLAUDE_PROJECT_DIR, projektowo przenośne między projektami.
  2. Stworzyć .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).
  3. Stworzyć .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.
  4. Dodać docs/notes/ do .gitignore. Notatki są ulotne + prywatne, nie są częścią repo.
  5. Ręczny smoke test: 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:

  • Skrypty Python: bezstanowy helper, honoruje CLAUDE_PROJECT_DIR
  • Hook Claude Code: zdarzenia wewnątrz narzędzia (mięso sesji)
  • Hook git: zdarzenia agnostyczne narzędziowo (szkielet commita)
  • gitignore: kontrola szumu

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