La parte più dura degli articoli caso reale è raccogliere il materiale: il git log ha solo commit message, i dettagli di sessione spariscono. Quattro hook e due slash command trasformano ogni sessione in raw.md — costo marginale zero.
La parte più difficile nello scrivere un articolo "Claude ha fatto X per me" non è scriverlo — è raccogliere il materiale. Il git log ha solo i messaggi di commit; le proposte che hai scartato, i tentativi falliti, le tre revisioni di quel prompt, l'output d'errore — è quello che rende un articolo leggibile, ed è tutto perso.
Peggio: quando ti metti a scrivere, non ricordi più perché hai scelto A invece di B. "Mi pare avessi una ragione" vs "quella ragione era X" è la differenza fra un articolo credibile e riempitivo.
Prendere note di riunione costa più che scrivere l'articolo, quindi non le prendi. L'unica soluzione è lasciare che la registrazione avvenga da sé — gli hook.
Ho fatto scrivere a Claude 4 hook + 2 slash command che trasformano ogni sessione di dev in docs/notes/<feature>/raw.md. L'articolo precedente, Lasciare che Claude faccia il deploy in produzione, ha tirato la maggior parte dei commit, snippet di bash e riferimenti a errori da questa pipeline. Anche questo.
4 hook + 2 slash command manuali formano un'unica catena:
| Trigger | Cosa fa |
|---|---|
| PostToolUse(Edit\ | Write\ |
| PostToolUse(Bash) | Ferma quando torni su master + pulito |
| git post-commit | Scrive un checkpoint per ogni commit |
| Stop | Parse del transcript a fine sessione |
/record-feature NAME /stop-recording |
Override manuale |
Un solo file di stato: docs/notes/.state.json. Ogni hook lo legge e lo scrive. Nessun altro coordinamento.
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/bin/recording-state maybe-start"
}]
}
]
Nucleo di maybe-start:
def cmd_maybe_start():
if load_state(): return # già in registrazione
if not tree_is_dirty(): return # niente cambiato, salta
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") restituisce "pro". I dev già partizionano il lavoro per branch, quindi riutilizziamo quel confine mentale — la ragione di fondo per cui questa pipeline sopravvive è che non chiede all'utente di ricordare niente di nuovo. Su master, gli script usa-e-getta cadono su un timestamp.
Perché il matcher è Edit/Write/MultiEdit: scrivere codice è il vero segnale "sto effettivamente facendo qualcosa". Leggere file, lanciare test, fare domande — non conta.
{
"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()
Tre cancelli: le registrazioni manuali non si fermano, le feature branch non si fermano, tree sporco non si ferma.
Perché appeso a Bash e non a Edit: dopo il merge in master di solito non editi più, ma lancerai sicuramente git status / git log / bin/rails test — qualsiasi comando dà all'hook la possibilità di notare "è ora di chiudere".
La flag auto_started è portante. Se avvii /record-feature pro-launch e attraversi più branch su più merge, le regole di auto-stop ucciderebbero la registrazione a metà feature. Le registrazioni manuali non si auto-fermano mai — solo /stop-recording.
Non è un hook di Claude Code — è .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 aggiunge il messaggio di commit completo a 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
> ...
Perché i messaggi di commit contano: sono il riassunto scritto a mano che tu (o Claude) hai prodotto esattamente nel momento in cui la feature era appena finita e il contesto era completo. Più accurato del ricordo successivo, più corto del transcript, più astratto del diff.
Quando Claude scrive un commit message, sta essenzialmente scrivendo materiale di riassunto per il tuo articolo futuro — basta che lo faccia bene al primo colpo, senza riscrivere dopo.
"Stop": [{
"hooks": [{ "command": "$CLAUDE_PROJECT_DIR/bin/extract-session-notes" }]
}]
Quando una sessione finisce, Claude Code passa transcript_path all'hook via stdin JSON. extract-session-notes apre quel jsonl, lo scorre riga per riga e smista:
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})
Solo i comandi bash in whitelist vengono catturati. Il 90% del bash in una sessione di codice è ls / cat / grep / head — gli articoli non ne usano nulla, tutto filtrato. Ciò che resta (lanciare test, kamal, rails runner, curl contro API) sono i comandi con una storia dietro.
I prompt utente avvolti in <command-*> o <system-*> vengono scartati — solo l'input reale sopravvive. I path Edit/Write deduplicati in un set. L'output d'errore tagliato a 400 caratteri. Le chiamate Task sub-agent mantenute (prompt tagliato a 2000 caratteri, sufficiente per vedere cosa ha fatto il sub-agent).
L'hook Stop non si attiva una volta per registrazione — si attiva ogni volta che una sessione finisce. Sulla stessa feature branch puoi aprire e chiudere Claude Code cinque o sei volte. Se ogni Stop re-parsasse tutto il transcript e scrivesse tutto, raw.md affogherebbe in duplicati.
Soluzione: mettere un cursore last_extracted_at nel file di stato:
filter_after = state.get("last_extracted_at") or state.get("started_at")
events = extract_events(transcript_path, filter_after)
# ...dopo la scrittura...
state["last_extracted_at"] = datetime.now().astimezone().isoformat()
save_state(project_dir, state)
Ogni passata prende solo gli eventi successivi al cursore. Semplice, ma dimenticalo e ottieni centinaia di righe duplicate.
/record-feature NAME:
{
"feature": "NAME",
"started_at": "ISO timestamp",
"branch": "current branch",
"auto_started": false
}
Quella riga auto_started: false disabilita l'auto-stop. Casi d'uso: scrivere uno script usa-e-getta su master volendo comunque una traccia, una feature che attraversa più branch, o dichiarare esplicitamente "questo è per un articolo".
/stop-recording: lancia un'estrazione finale sul jsonl più recente, poi pulisce il file di stato.
Frammento reale (da 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)
Per scrivere un articolo: basta grep. "Quale commit era il fix del wallet address?" → eba9ac9. "Come ho prompt-ato il sub-agent del refactor?" → sotto ### Sub-agent invocations.
docs/notes/ è in .gitignore — è materiale di bozza per la tua scrittura, non codice sorgente.
1. La branch come confine di feature, non un sistema di metadata separato. I dev già partizionano il lavoro per branch; aggiungere un'altra layer "nome feature" andrebbe inevitabilmente alla deriva. Riutilizzare il confine esistente, carico cognitivo zero.
2. Whitelist per bash, non blacklist. Il 95% del bash nei transcript è rumore. Mantenere una lista "cosa vale la pena conservare" resta stabile per anni; mantenere una lista "cosa filtrare" no.
3. Il cursore vive nel file di stato, non nel transcript. Il transcript appartiene a Claude Code; lo stato appartiene alla pipeline. Disaccoppiati — Claude Code può cambiare il formato del transcript senza rompermi, e io posso riscrivere la logica di estrazione senza toccare i transcript.
Scrivere un articolo da 1500 parole consuma materiale da circa 30 commit, 4 sessioni, una dozzina di comandi bash chiave. Raccoglierlo a mano costa un'ora — abbastanza perché la prossima volta tu tagli gli angoli.
Lascia fare agli hook: costo marginale zero. Installa questo una volta e lavora semplicemente come sempre — tira una branch, scrivi codice, rilascia — e il materiale per l'articolo si accumula da sé.
Ogni riferimento di commit, snippet di bash, riga di errore e path di file in questo articolo viene da docs/notes/pro/raw.md — materiale di bozza che l'hook ha registrato da sé dal momento in cui ho tirato feature/pro due giorni fa. Quando è arrivato il momento di scrivere, ho aperto il file e c'era tutto quello che serviva.
Il modo migliore per far scrivere a Claude articoli su Claude Code è prima fargli scrivere hook per registrare se stesso.