Il sistema di registrazione di how2claude su un altro progetto Rails: 4 file, 5 mosse, layering degli hook.
how2claude ha una serie di hooks che registrano automaticamente le session di Claude Code — fa partire la registrazione all'inizio del lavoro, aggiunge un checkpoint a ogni commit e, a fine session, estrae prompt / bash / lista di edit in docs/notes/<feature>/raw.md. L'articolo let-claude-record-itself ha raccontato come è stato costruito.
Problema: l'altro mio progetto (smarts, un sito di docs per smart contract) non aveva nulla di tutto ciò. Ogni volta che volevo scrivere un articolo dopo, scavavo in git log e memoria, sentendomi sempre sfuggire il bello. Questo articolo racconta come ho portato il sistema di registrazione — 4 file, 5 minuti in totale — ma lungo la strada è emerso un insight vero sul layering degli hooks: gli hooks di Claude Code e gli hooks di git operano su livelli completamente diversi, e cosa viene catturato mescolando strumenti (Amp + Claude Code, per esempio) dipende da dove installi.
Tutto ciò che in how2claude riguarda la registrazione:
| File | Livello | Ruolo |
|---|---|---|
bin/recording-state |
script | helper Python, gestisce il ciclo di vita di .state.json |
bin/extract-session-notes |
script | helper Python, legge il transcript di Claude Code → scrive raw.md |
.claude/settings.local.json |
hook Claude Code | PostToolUse / Stop attivano gli script sopra |
.git/hooks/post-commit |
hook git | ogni commit chiama recording-state commit per un checkpoint |
.gitignore |
controllo rumore | tiene docs/notes/ fuori dalla repo (le note sono private/transitorie) |
4 pezzi, 4 livelli distinti. Questa distinzione torna sotto più volte.
Due sistemi di hook girano insieme, con scope molto diversi:
Hooks Claude Code (definiti in .claude/settings.local.json):
- Scope: scattano solo dentro lo strumento Claude Code
- Trigger: PostToolUse / Stop / PreToolUse — eventi del ciclo di vita di Claude Code
- Info disponibili: nome tool, args, transcript_path (jsonl completo della session) — cose che solo Claude Code conosce
Hooks git (script shell sotto .git/hooks/):
- Scope: scattano su ogni evento git, indipendentemente da chi ha attivato git
- Trigger: post-commit / pre-push / ecc.
- Info disponibili: quel che git stesso sa (sha, autore, branch, diff)
Conseguenza reale: scrivere codice + commit dentro Claude Code scatena entrambi i livelli — info della session e info del commit atterrano in raw.md. Passare a Amp (o Cursor, o digitazione a mano) per scrivere + commit e scatta solo l'hook git — raw.md riceve lo scheletro del commit ma nessun prompt / bash / dettaglio edit della session.
Non è un bug — è il vincolo di design di ciascuno strumento. Volere dettaglio a livello session sotto ogni strumento significa installare il proprio livello di hook per strumento. La rete di sicurezza di git ti dà il "cosa è stato fatto"; non ti dà il "cosa è stato pensato, dove si è rotto".
Guida alla scelta:
- Scheletro agnostico rispetto allo strumento (info commit, cambiamenti di codice) → mettilo in un hook git
- Carne specifica di Claude Code (prompt completi, ragionamento) → mettilo in un hook Claude Code
- Entrambi → installa su entrambi i livelli
Installare lo stesso setup di registrazione in smarts (/home/bob/Work/smarts, progetto Rails).
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}
Script senza modifiche — usano la variabile d'ambiente $CLAUDE_PROJECT_DIR per decidere dove scrivere:
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"
Questa è l'astrazione chiave: Claude Code imposta CLAUDE_PROJECT_DIR automaticamente quando fa scattare l'hook; noi la impostiamo manualmente dentro il post-commit git. Entrambi i lati rispettano la stessa env var, e lo script non deve capire "in che progetto sono".
.claude/settings.local.jsonUna decisione qui: portare solo gli hooks, non la lista di permissions.
Il settings.local.json di how2claude ha più di 100 voci permissions.allow — tutte specifiche di how2claude (curl localhost:3000, bin/rails runner, kamal app exec). Zero senso trasportarle su smarts. smarts accumulerà le proprie permissions organicamente man mano che lo usi.
Gli hooks sono pattern — identici tra progetti. Le permissions sono stato di progetto — diverse tra progetti.
{
"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" }
]
}
]
}
}
Tre punti di trigger:
- File modificato → prova ad avviare registrazione (maybe-start si autocontrolla: se già in registrazione salta, se tree pulito salta anche)
- Comando bash eseguito → prova a fermare (maybe-stop è rigoroso: ferma solo se "autoavviato E tornato su master E tree pulito" valgono tutti insieme)
- Session finita → estrai transcript in raw.md
.git/hooks/post-commit3 righe:
#!/bin/bash
ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
CLAUDE_PROJECT_DIR="$ROOT" "$ROOT/bin/recording-state" commit || true
L'export manuale CLAUDE_PROJECT_DIR=$ROOT è il punto esatto dove il mondo git e il mondo Claude Code vengono messi in ponte dalla stessa env var. Il || true garantisce che l'hook non blocchi mai un commit.
chmod +x /home/bob/Work/smarts/.git/hooks/post-commit
docs/notes/ al .gitignore# Session recording notes (transient, for article material)
docs/notes/
Le note sono transitorie + private — non vuoi raw.md commitato nei PR; non vuoi .state.json che sporca git status. Il gitignore di how2claude gestisce la cosa allo stesso modo.
Attivare maybe-start manualmente subito dopo l'installazione:
$ 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
}
Lo script ha correttamente visto che smarts si trovava casualmente su feat/contract-to-docs con tree sporco — ha avviato la registrazione in automatico, ha dedotto il nome feature contract-to-docs dal branch feat/contract-to-docs. Quella logica nello script:
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/X → X, feature/X → X, fix-y nudo → fix-y, master/main → None (che ripiega su un nome con timestamp session-YYYYMMDD-HHMM).
L'euristica è deliberatamente sciocca al punto giusto — il nome del branch è l'argomento del lavoro, non c'è bisogno di farti rinominare.
Dopo l'installazione mi sono chiesto: ogni tanto lavoro in Amp — quella parte viene catturata? La risposta è esattamente quello che la storia del layering degli hook predice:
| Scenario | Hook Claude Code | Hook git | Cosa atterra nelle note |
|---|---|---|---|
| Lavoro Claude Code + commit | ✅ scatta | ✅ scatta | dettaglio session + scheletro commit |
| Lavoro Amp + commit | ❌ no-op | ✅ scatta | solo scheletro commit |
| Digitazione manuale + commit | ❌ no-op | ✅ scatta | solo scheletro commit |
| Lavoro Claude Code, senza commit ancora | ✅ avvia registrazione | — | dettaglio session (voce commit aspetta prossimo commit) |
Conclusione: basta se Claude Code è il conducente principale. Lo scheletro del commit atterra sempre; il dettaglio della session atterra solo sulla rotta Claude Code. Per articoli "cosa è stato fatto" ci si appoggia soprattutto ai body dei commit — i prompt / tracce bash della session sono bonus, belli da avere ma non indispensabili.
Se usi Amp pesante, Amp ha un proprio sistema di hook (non sono sceso nei dettagli); un piccolo script di inoltro che fa scattare recording-state maybe-start/maybe-stop funzionerebbe allo stesso modo.
Portare il session recording di Claude Code su un altro progetto — le 5 mosse:
cp due script Python nel bin/ del progetto di destinazione. Nessuna modifica — gli script rispettano $CLAUDE_PROJECT_DIR, portabili tra progetti per design..claude/settings.local.json, solo hooks. Non portare la lista di permissions — le permissions sono stato di progetto (diverse per progetto); gli hooks sono pattern (identici tra progetti)..git/hooks/post-commit (3 righe), export CLAUDE_PROJECT_DIR=$ROOT a mano e chiamare recording-state commit. È l'unico punto dove mondo git e mondo Claude Code vengono messi in ponte dalla stessa env var.docs/notes/ al .gitignore. Le note sono transitorie + private, non parte della repo.CLAUDE_PROJECT_DIR=$(pwd) ./bin/recording-state maybe-start, verificare che l'inferenza branch→feature sia giusta. Se il tree è pulito, lo script è progettato per saltare — non è un bug.La vera decisione di design non è "come lo porto" — il portaggio è quasi cp. È suddividere la logica di registrazione in 4 livelli distinti, ciascuno facendo una cosa chiara:
CLAUDE_PROJECT_DIRCiascuno dei 4 livelli può essere portato, sostituito o saltato in modo indipendente (vuoi supporto Amp? aggiungi un livello hook Amp. Cambi formato note? modifica il Python. Non vuoi che git tracci le note? elimina il post-commit). Claude può scrivere il codice bene — ma il giudizio su "a quale livello appartiene questa funzionalità" non può farlo per te. È un giudizio sui confini del tuo tooling, ed è tuo.