Free

Claude Hooks schreiben lassen, die ihn selbst aufzeichnen

Das Schwerste an Fallstudien-Artikeln ist das Material zu sammeln: git log hat nur Commit-Messages, Session-Details verschwinden. Vier Hooks und zwei Slash-Commands machen aus jeder Session eine raw.md — Grenzkosten null.


Das Härteste beim Schreiben eines "Claude hat X für mich gemacht"-Artikels ist nicht das Schreiben — es ist das Material zusammenzusuchen. Der git log hat nur Commit-Nachrichten; die Ansätze, die du verworfen hast, die gescheiterten Versuche, die drei Überarbeitungen dieses einen Prompts, die Fehlerausgabe — das ist, was einen Artikel lesenswert macht, und das ist alles weg.

Schlimmer: Zum Zeitpunkt, wo du schreibst, erinnerst du dich nicht mehr, warum du A statt B gewählt hast. "Ich glaube, es gab einen Grund" vs. "dieser Grund war X" ist der Unterschied zwischen einem glaubwürdigen Artikel und Füllwerk.

Protokolle zu schreiben kostet mehr als den Artikel selbst, also schreibst du keine. Die einzige Lösung ist, die Aufzeichnung von selbst passieren zu lassen — Hooks.

Ich habe Claude 4 Hooks + 2 Slash-Commands schreiben lassen, die jede Dev-Session in docs/notes/<feature>/raw.md verwandeln. Der vorherige Artikel Claude in Produktion deployen lassen hat die meisten seiner Commits, Bash-Snippets und Fehlerreferenzen aus dieser Pipeline gezogen. Dieser auch.


Pipeline-Überblick

4 Hooks + 2 manuelle Slash-Commands bilden ein einziges Band:

Trigger Was er tut
PostToolUse(Edit\ Write\
PostToolUse(Bash) Stoppt, wenn zurück auf master + sauber
git post-commit Schreibt pro Commit einen Checkpoint
Stop Parst das Transcript am Session-Ende
/record-feature NAME /stop-recording Manuelles Override

Eine State-Datei: docs/notes/.state.json. Jeder Hook liest und schreibt sie. Keine weitere Koordination.

Hook #1: wann anfangen

"PostToolUse": [
  {
    "matcher": "Edit|Write|MultiEdit",
    "hooks": [{
      "type": "command",
      "command": "$CLAUDE_PROJECT_DIR/bin/recording-state maybe-start"
    }]
  }
]

Kern von maybe-start:

def cmd_maybe_start():
    if load_state(): return          # bereits aufzeichnend
    if not tree_is_dirty(): return   # nichts verändert, skip
    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") gibt "pro" zurück. Entwickler teilen die Arbeit ohnehin per Branch, also nutzen wir diese mentale Grenze wieder — der eigentliche Grund, warum diese Pipeline überlebt, ist, dass sie dem User nichts Neues abverlangt, das er sich merken müsste. Auf master fallen einmalige Scripts auf einen Timestamp zurück.

Warum der Matcher Edit/Write/MultiEdit ist: Code schreiben ist das eigentliche "ich mache wirklich etwas"-Signal. Dateien lesen, Tests laufen lassen, Fragen stellen — zählt nicht.

Hook #2: wann aufhören

{
  "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()

Drei Tore: manuelle Aufzeichnungen stoppen nicht, Feature-Branches stoppen nicht, dirty Trees stoppen nicht.

Warum an Bash gehängt, nicht an Edit: nach dem Merge auf master editiert man üblicherweise nicht mehr, aber man wird sicher git status / git log / bin/rails test laufen lassen — irgendein Befehl gibt dem Hook die Chance zu bemerken "Zeit zum Abschließen".

Die auto_started-Flag ist tragend. Wenn du /record-feature pro-launch startest und mehrere Branches über mehrere Merges durchläufst, würden Auto-Stop-Regeln die Aufzeichnung mitten im Feature killen. Manuelle Aufzeichnungen stoppen nie automatisch — nur /stop-recording.

Hook #3: Commit als Checkpoint

Kein Claude Code Hook — das ist .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 hängt die komplette Commit-Nachricht an raw.md an:

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

Warum Commit-Nachrichten zählen: Sie sind die handgeschriebene Zusammenfassung, die du (oder Claude) genau in dem Moment produziert hat, als das Feature gerade fertig war und der Kontext vollständig. Genauer als späteres Erinnern, kürzer als das Transcript, abstrakter als der Diff.

Wenn Claude eine Commit-Nachricht schreibt, schreibt er im Grunde schon Zusammenfassungsmaterial für deinen zukünftigen Artikel — du brauchst ihn nur beim ersten Mal gut, kein späteres Umschreiben.

Hook #4: Events aus dem Transcript extrahieren

"Stop": [{
  "hooks": [{ "command": "$CLAUDE_PROJECT_DIR/bin/extract-session-notes" }]
}]

Wenn eine Session endet, reicht Claude Code transcript_path per stdin-JSON an den Hook weiter. extract-session-notes öffnet diese jsonl, geht sie Zeile für Zeile durch und sortiert in Eimer:

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

Nur Bash-Befehle aus der Whitelist werden eingefangen. 90% des Bash einer Code-Session ist ls / cat / grep / head — benutzt kein Artikel, alles rausgefiltert. Was übrig bleibt (Tests laufen lassen, kamal, rails runner, curl gegen APIs) sind die Befehle mit Geschichte dahinter.

User-Prompts in <command-*> oder <system-*> werden verworfen — nur echte Eingabe überlebt. Edit/Write-Pfade werden in einem Set dedupliziert. Fehlerausgabe bei 400 Zeichen gekappt. Task-Sub-Agent-Aufrufe werden behalten (Prompt bei 2000 Zeichen abgeschnitten, reicht um zu sehen, was der Sub-Agent gemacht hat).

Eine Aufzeichnung, viele Stops: nutze einen Cursor

Der Stop-Hook feuert nicht einmal pro Aufzeichnung — er feuert jedes Mal, wenn eine Session endet. Auf derselben Feature-Branch kannst du Claude Code fünf- oder sechsmal öffnen und schließen. Würde jedes Stop das ganze Transcript neu parsen und alles schreiben, würde raw.md in Duplikaten ertrinken.

Lösung: einen Cursor last_extracted_at in die State-Datei setzen:

filter_after = state.get("last_extracted_at") or state.get("started_at")
events = extract_events(transcript_path, filter_after)
# ...nach dem Schreiben...
state["last_extracted_at"] = datetime.now().astimezone().isoformat()
save_state(project_dir, state)

Jeder Durchlauf nimmt nur Events nach dem Cursor. Einfach, aber vergiss ihn, und es regnen hunderte Duplikat-Zeilen.

Manuelles Override: zwei Slash-Commands

/record-feature NAME:

{
  "feature": "NAME",
  "started_at": "ISO timestamp",
  "branch": "current branch",
  "auto_started": false
}

Die Zeile auto_started: false deaktiviert den Auto-Stop. Einsatzfälle: ein Einmal-Script auf master schreiben, aber trotzdem Spuren hinterlassen wollen; ein Feature, das mehrere Branches umspannt; explizit erklären "das ist für einen Artikel".

/stop-recording: führt eine finale Extraktion gegen die neueste jsonl aus und räumt die State-Datei auf.

Wie raw.md aussieht

Echter Ausschnitt (aus 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)

Zum Schreiben eines Artikels: einfach grep. "Welcher Commit war der Wallet-Address-Fix?" → eba9ac9. "Wie habe ich den Refactor-Sub-Agent geprompted?" → unter ### Sub-agent invocations.

docs/notes/ ist in .gitignore — das ist Entwurfsmaterial für deine Schreibarbeit, kein Quellcode.

Drei nicht offensichtliche Design-Entscheidungen

1. Branch als Feature-Grenze, kein separates Metadata-System. Entwickler partitionieren Arbeit ohnehin per Branch; eine weitere "Feature-Name"-Ebene würde unweigerlich driften. Vorhandene Grenze wiederverwenden, null kognitive Last.

2. Whitelist für Bash, keine Blacklist. 95% vom Bash in Transcripts ist Rauschen. Eine Liste "was es wert ist, behalten zu werden" bleibt über Jahre stabil; eine Liste "was zu filtern ist" nicht.

3. Der Cursor lebt in der State-Datei, nicht im Transcript. Das Transcript gehört Claude Code; der State gehört der Pipeline. Entkoppelt — Claude Code kann das Transcript-Format ändern, ohne mich zu brechen, und ich kann die Extraktionslogik umschreiben, ohne Transcripts anzufassen.

Die Schleife schließen

Einen 1500-Wörter-Artikel zu schreiben verbraucht Material von etwa 30 Commits, 4 Sessions, einem Dutzend wichtiger Bash-Befehle. Das per Hand einzusammeln kostet ungefähr eine Stunde — genug, dass du beim nächsten Mal abkürzt.

Lass Hooks das machen: Grenzkosten null. Das einmal installieren und einfach normal arbeiten — eine Branch ziehen, Code schreiben, deployen — und das Artikel-Material häuft sich von selbst auf.

Jede Commit-Referenz, jedes Bash-Snippet, jede Fehlerzeile und jeder Dateipfad in diesem Artikel kam aus docs/notes/pro/raw.md — Entwurfsmaterial, das der Hook selbst aufgezeichnet hat, ab dem Moment, als ich vor zwei Tagen feature/pro gezogen habe. Als es ans Schreiben ging, öffnete ich die Datei und alles, was ich brauchte, war da.

Der beste Weg, Claude Artikel über Claude Code schreiben zu lassen, ist, Claude zuerst Hooks schreiben zu lassen, die ihn selbst aufzeichnen.