Free

Portare il session recording di Claude Code su un secondo progetto

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.


4 file, una sola immagine

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.

L'insight chiave: il layering degli hooks decide cosa viene catturato

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

Portare in 5 mosse

Installare lo stesso setup di registrazione in smarts (/home/bob/Work/smarts, progetto Rails).

1. Copiare i due script 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}

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

2. Creare .claude/settings.local.json

Una 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

3. Creare .git/hooks/post-commit

3 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

4. Aggiungere 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.

5. Smoke test (con piccola sorpresa)

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/XX, feature/XX, 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.

Confine di visibilità mescolando con Amp

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.

Checklist

Portare il session recording di Claude Code su un altro progetto — le 5 mosse:

  1. cp due script Python nel bin/ del progetto di destinazione. Nessuna modifica — gli script rispettano $CLAUDE_PROJECT_DIR, portabili tra progetti per design.
  2. Creare .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).
  3. Creare .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.
  4. Aggiungere docs/notes/ al .gitignore. Le note sono transitorie + private, non parte della repo.
  5. Smoke test manuale: 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:

  • Script Python: helper senza stato, rispetta CLAUDE_PROJECT_DIR
  • Hook Claude Code: eventi interni allo strumento (carne della session)
  • Hook git: eventi agnostici rispetto allo strumento (scheletro del commit)
  • gitignore: controllo del rumore

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