Free

Das Claude-Code-Session-Recording in ein zweites Projekt portieren

how2claudes Recording-System auf ein anderes Rails-Projekt: 4 Dateien, 5 Schritte, Hook-Layering.


how2claude hat ein Set von Hooks, die Claude-Code-Sessions automatisch aufzeichnen — startet das Recording, sobald Arbeit beginnt, hängt bei jedem Commit einen Checkpoint an und extrahiert am Session-Ende Prompts / Bash / Edit-Listen nach docs/notes/<feature>/raw.md. Der Artikel let-claude-record-itself beschrieb den Aufbau.

Problem: Mein anderes Projekt (smarts, eine Smart-Contract-Docs-Site) hatte nichts davon. Jedes Mal wenn ich hinterher einen Artikel schreiben wollte, wühlte ich in git log und Erinnerung und hatte das Gefühl, das Beste zu verpassen. Dieser Artikel erzählt, wie ich das Recording-System rübergeportet habe — 4 Dateien, 5 Minuten insgesamt — doch unterwegs kam eine echte Einsicht zum Hook-Layering zu Tage: Claude-Code-Hooks und Git-Hooks operieren auf völlig verschiedenen Ebenen, und was beim Mischen von Werkzeugen (z. B. Amp + Claude Code) erfasst wird, hängt davon ab, auf welcher Ebene man installiert.


4 Dateien, ein Bild

Alles in how2claude rund ums Recording:

Datei Ebene Rolle
bin/recording-state Skript Python-Helper, verwaltet den Lebenszyklus von .state.json
bin/extract-session-notes Skript Python-Helper, liest das Claude-Code-Transcript → schreibt raw.md
.claude/settings.local.json Claude-Code-Hook PostToolUse / Stop lösen die obigen Skripte aus
.git/hooks/post-commit Git-Hook jeder Commit ruft recording-state commit für einen Checkpoint
.gitignore Lärmkontrolle hält docs/notes/ aus dem Repo raus (Notizen sind privat/flüchtig)

4 Teile, 4 verschiedene Ebenen. Diese Unterscheidung kommt unten mehrfach zurück.

Die Schlüsseleinsicht: Hook-Layering entscheidet, was erfasst wird

Zwei Hook-Systeme laufen gleichzeitig, mit völlig unterschiedlichen Scopes:

Claude-Code-Hooks (definiert in .claude/settings.local.json):
- Scope: feuern nur innerhalb des Claude-Code-Werkzeugs
- Trigger: PostToolUse / Stop / PreToolUse — Claude-Code-Lebenszyklusevents
- Verfügbare Infos: Tool-Name, Args, transcript_path (vollständiges Session-jsonl) — Dinge, die nur Claude Code kennt

Git-Hooks (Shell-Skripte unter .git/hooks/):
- Scope: feuern bei jedem Git-Event, egal wer Git ausgelöst hat
- Trigger: post-commit / pre-push / etc.
- Verfügbare Infos: was Git selbst weiß (SHA, Autor, Branch, Diff)

Reale Konsequenz: Code schreiben + committen in Claude Code feuert beide Ebenen — Session-Info und Commit-Info landen in raw.md. Auf Amp (oder Cursor oder manuelles Tippen) umschalten zum Schreiben + Committen und nur der Git-Hook feuert — raw.md bekommt das Commit-Gerüst, aber keine Prompt/Bash/Edit-Details der Session.

Das ist kein Bug — es ist die Design-Einschränkung jedes Werkzeugs. Wenn man unter allen Werkzeugen Session-Detail will, muss man pro Werkzeug seine eigene Hook-Schicht installieren. Das Git-Sicherungsnetz gibt dir „was wurde getan"; es gibt dir nicht „was wurde gedacht, woran ist es gescheitert".

Auswahlhilfe:
- Werkzeugunabhängiges Gerüst (Commit-Info, Code-Änderungen) → in einen Git-Hook
- Claude-Code-spezifisches Fleisch (vollständige Prompts, Denkprozess) → in einen Claude-Code-Hook
- Beides → auf beiden Ebenen installieren

Portierung in 5 Zügen

Das gleiche Recording-Setup in smarts (/home/bob/Work/smarts, ein Rails-Projekt) installieren.

1. Die zwei Python-Skripte kopieren

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}

Skripte unverändert — sie nutzen die Env-Var $CLAUDE_PROJECT_DIR, um zu entscheiden, wohin geschrieben wird:

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"

Das ist die Schlüsselabstraktion: Claude Code setzt CLAUDE_PROJECT_DIR automatisch beim Hook-Auslösen; wir setzen es manuell im Git-Post-Commit. Beide Seiten respektieren dieselbe Env-Var, und das Skript muss nicht herausfinden „in welchem Projekt bin ich".

2. .claude/settings.local.json anlegen

Eine Entscheidung hier: nur die Hooks portieren, nicht die Permissions-Liste.

how2claudes settings.local.json hat über 100 permissions.allow-Einträge — alle spezifisch für how2claude (curl localhost:3000, bin/rails runner, kamal app exec). Null Sinn, das auf smarts zu ziehen. smarts wird beim Benutzen organisch seine eigenen Permissions aufbauen.

Hooks sind Muster — projektübergreifend identisch. Permissions sind Projektzustand — projektübergreifend verschieden.

{
  "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" }
        ]
      }
    ]
  }
}

Drei Trigger-Punkte:
- Datei editiert → versuche, Recording zu starten (maybe-start prüft sich selbst: wenn schon am Aufnehmen, überspringen; wenn Tree clean, auch überspringen)
- Bash-Kommando gelaufen → versuche zu stoppen (maybe-stop ist streng: stoppt nur, wenn „automatisch gestartet UND zurück auf Master UND Tree clean" alle gleichzeitig gelten)
- Session endet → Transcript nach raw.md extrahieren

3. .git/hooks/post-commit anlegen

3 Zeilen:

#!/bin/bash
ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
CLAUDE_PROJECT_DIR="$ROOT" "$ROOT/bin/recording-state" commit || true

Das manuelle CLAUDE_PROJECT_DIR=$ROOT-Export ist genau der Punkt, an dem Git-Welt und Claude-Code-Welt über dieselbe Env-Var verbunden werden. Das || true garantiert, dass der Hook einen Commit nie blockiert.

chmod +x /home/bob/Work/smarts/.git/hooks/post-commit

4. docs/notes/ zur .gitignore hinzufügen

# Session recording notes (transient, for article material)
docs/notes/

Notizen sind flüchtig + privat — du willst nicht, dass raw.md in PRs committet wird; du willst nicht, dass .state.json git status zumüllt. how2claudes Gitignore handhabt es genauso.

5. Smoke-Test (mit kleiner Überraschung)

maybe-start direkt nach der Installation manuell auslösen:

$ 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
}

Das Skript erkannte korrekt, dass smarts gerade zufällig auf feat/contract-to-docs mit schmutzigem Tree stand — startete das Recording automatisch, leitete den Feature-Namen contract-to-docs aus dem Branch feat/contract-to-docs ab. Diese Logik im Skript:

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, nacktes fix-yfix-y, master/main → None (fällt zurück auf einen Zeitstempel-Namen session-YYYYMMDD-HHMM).

Die Heuristik ist bewusst simpel — der Branch-Name ist das Arbeitsthema, keine Notwendigkeit, dich nochmal einen Namen geben zu lassen.

Sichtbarkeitsgrenze beim Mischen mit Amp

Nach der Installation fragte ich mich: Ich arbeite gelegentlich in Amp — wird das erfasst? Die Antwort ist genau, was die Hook-Layering-Story vorhersagt:

Szenario Claude-Code-Hook Git-Hook Was landet in den Notizen
Arbeit in Claude Code + Commit ✅ feuert ✅ feuert Session-Detail + Commit-Gerüst
Arbeit in Amp + Commit ❌ No-op ✅ feuert nur Commit-Gerüst
Handgetippt + Commit ❌ No-op ✅ feuert nur Commit-Gerüst
Arbeit in Claude Code, noch kein Commit ✅ startet Recording Session-Detail (Commit-Eintrag wartet auf nächsten Commit)

Fazit: Reicht, wenn Claude Code der Hauptfahrer ist. Commit-Gerüst landet immer; Session-Detail landet nur auf dem Claude-Code-Pfad. Für „was wurde getan"-Artikel verlässt man sich hauptsächlich auf Commit-Bodies — Session-Prompts / Bash-Spuren sind Bonus, schön wenn vorhanden, aber nicht zwingend.

Wenn du Amp stark nutzt, hat Amp sein eigenes Hook-System (ich habe nicht in die Details geschaut); ein kleines Weiterleitungs-Skript, das recording-state maybe-start/maybe-stop auslöst, würde genauso funktionieren.

Checkliste

Claude-Code-Session-Recording in ein anderes Projekt portieren — die 5 Züge:

  1. Die zwei Python-Skripte cp ins bin/ des Zielprojekts. Ohne Modifikation — Skripte respektieren $CLAUDE_PROJECT_DIR, von Haus aus projektübergreifend portabel.
  2. .claude/settings.local.json anlegen, nur Hooks. Permissions-Liste nicht portieren — Permissions sind Projektzustand (pro Projekt verschieden); Hooks sind Muster (projektübergreifend identisch).
  3. .git/hooks/post-commit anlegen (3 Zeilen), manuell export CLAUDE_PROJECT_DIR=$ROOT und recording-state commit aufrufen. Das ist der einzige Punkt, an dem Git-Welt und Claude-Code-Welt über dieselbe Env-Var verbunden werden.
  4. docs/notes/ zur .gitignore hinzufügen. Notizen sind flüchtig + privat, nicht Teil des Repos.
  5. Manueller Smoke-Test: CLAUDE_PROJECT_DIR=$(pwd) ./bin/recording-state maybe-start, prüfen, dass die Branch-zu-Feature-Inferenz korrekt ist. Wenn Tree clean ist, ist das Skript so ausgelegt, zu überspringen — kein Bug.

Die echte Design-Entscheidung ist nicht „wie portiere ich" — Portieren ist fast cp. Sie ist, die Recording-Logik in 4 verschiedene Ebenen aufzuteilen, wobei jede Ebene eine klare Sache tut:

  • Python-Skripte: zustandsloser Helper, respektiert CLAUDE_PROJECT_DIR
  • Claude-Code-Hook: werkzeuginterne Events (Session-Fleisch)
  • Git-Hook: werkzeugunabhängige Events (Commit-Gerüst)
  • gitignore: Lärmkontrolle

Jede der 4 kann unabhängig portiert, ersetzt oder übersprungen werden (Amp-Unterstützung gewünscht? Amp-Hook-Ebene hinzufügen. Notiz-Format ändern? Python bearbeiten. Soll Git Notizen nicht verfolgen? Post-Commit löschen). Claude kann den Code richtig schreiben — aber das Urteil „zu welcher Ebene gehört diese Funktionalität" kann es nicht für dich fällen. Das ist ein Urteil über deine Werkzeuggrenzen, und es gehört dir.