Le système d'enregistrement de how2claude sur un autre projet Rails : 4 fichiers, 5 étapes, layering de hooks.
how2claude possède un ensemble de hooks qui enregistrent automatiquement les sessions Claude Code — démarre l'enregistrement quand le travail commence, ajoute un checkpoint à chaque commit, et à la fin de la session extrait les prompts / bash / listes d'édits vers docs/notes/<feature>/raw.md. L'article let-claude-record-itself a raconté comment c'était construit.
Problème : mon autre projet (smarts, un site de docs pour smart contracts) n'avait rien de tout ça. Chaque fois que je voulais écrire un article après coup, je déterrais git log et ma mémoire, avec le sentiment de passer à côté du plus intéressant. Cet article raconte comment j'ai porté le système d'enregistrement — 4 fichiers, 5 minutes au total — mais en chemin une vraie intuition sur le layering des hooks a émergé : les hooks Claude Code et les hooks git opèrent à des couches complètement différentes, et ce qui est capturé quand on mélange les outils (Amp + Claude Code, par exemple) dépend de la couche où on installe.
Tout ce qui concerne l'enregistrement dans how2claude :
| Fichier | Couche | Rôle |
|---|---|---|
bin/recording-state |
script | helper Python, gère le cycle de vie de .state.json |
bin/extract-session-notes |
script | helper Python, lit le transcript Claude Code → écrit raw.md |
.claude/settings.local.json |
hook Claude Code | PostToolUse / Stop déclenchent les scripts ci-dessus |
.git/hooks/post-commit |
hook git | chaque commit appelle recording-state commit pour un checkpoint |
.gitignore |
contrôle du bruit | garde docs/notes/ hors du repo (les notes sont privées/transitoires) |
4 pièces, 4 couches distinctes. Cette distinction revient plus bas.
Deux systèmes de hooks tournent en même temps, avec des portées très différentes :
Hooks Claude Code (définis dans .claude/settings.local.json) :
- Portée : ne se déclenchent qu'à l'intérieur de l'outil Claude Code
- Déclencheurs : PostToolUse / Stop / PreToolUse — événements du cycle de vie Claude Code
- Infos disponibles : nom de l'outil, args, transcript_path (jsonl complet de la session) — des choses que seul Claude Code connaît
Hooks git (scripts shell sous .git/hooks/) :
- Portée : se déclenchent sur chaque événement git, peu importe qui a déclenché git
- Déclencheurs : post-commit / pre-push / etc.
- Infos disponibles : ce que git sait (sha, auteur, branche, diff)
Conséquence réelle : écrire du code + commit dans Claude Code déclenche les deux couches — infos de session et infos de commit atterrissent dans raw.md. Basculer sur Amp (ou Cursor, ou taper à la main) pour écrire + commit et seul le hook git se déclenche — raw.md récupère le squelette de commit mais pas les prompts / bash / détails d'édit de la session.
Ce n'est pas un bug — c'est la contrainte de conception de chaque outil. Vouloir du détail au niveau session sous tous les outils signifie installer sa propre couche de hook par outil. Le filet de sécurité git te donne « ce qui a été fait » ; il ne te donne pas « ce qui a été pensé, ce qui a cassé ».
Guide de choix :
- Squelette agnostique de l'outil (infos de commit, changements de code) → dans un hook git
- Chair spécifique Claude Code (prompts complets, raisonnement) → dans un hook Claude Code
- Les deux → installer sur les deux couches
Installer la même config d'enregistrement dans smarts (/home/bob/Work/smarts, un projet 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}
Scripts sans modification — ils utilisent la variable d'env $CLAUDE_PROJECT_DIR pour décider où écrire :
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"
C'est l'abstraction clé : Claude Code positionne CLAUDE_PROJECT_DIR automatiquement lors du déclenchement des hooks ; on la positionne manuellement dans le post-commit git. Les deux côtés honorent la même variable d'env, et le script n'a pas besoin de deviner « dans quel projet je suis ».
.claude/settings.local.jsonUne décision ici : ne porter que les hooks, pas la liste de permissions.
Le settings.local.json de how2claude contient plus de 100 entrées permissions.allow — toutes spécifiques à how2claude (curl localhost:3000, bin/rails runner, kamal app exec). Zéro sens de les traîner vers smarts. smarts accumulera ses propres permissions organiquement au fur et à mesure.
Les hooks sont des patterns — identiques d'un projet à l'autre. Les permissions sont un état projet — différentes d'un projet à l'autre.
{
"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" }
]
}
]
}
}
Trois points de déclenchement :
- Fichier édité → tente de démarrer l'enregistrement (maybe-start s'auto-vérifie : si déjà en enregistrement, saute ; si tree propre, saute aussi)
- Commande bash exécutée → tente d'arrêter (maybe-stop est strict : n'arrête que si « démarré automatiquement ET de retour sur master ET tree propre » sont toutes vraies)
- Session terminée → extrait le transcript dans raw.md
.git/hooks/post-commit3 lignes :
#!/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 manuel CLAUDE_PROJECT_DIR=$ROOT est le point exact où le monde git et le monde Claude Code sont pontés par la même variable d'env. Le || true garantit que le hook ne bloque jamais un commit.
chmod +x /home/bob/Work/smarts/.git/hooks/post-commit
docs/notes/ au .gitignore# Session recording notes (transient, for article material)
docs/notes/
Les notes sont transitoires + privées — on ne veut pas que raw.md soit commité dans les PRs ; on ne veut pas que .state.json pollue git status. Le gitignore de how2claude le traite pareil.
Déclencher maybe-start manuellement juste après l'installation :
$ 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
}
Le script a correctement repéré que smarts se trouvait par hasard sur feat/contract-to-docs avec un tree sale — a démarré l'enregistrement automatiquement, a déduit le nom de feature contract-to-docs depuis la branche feat/contract-to-docs. La logique dans le 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 nu → fix-y, master/main → None (qui retombe sur un nom horodaté session-YYYYMMDD-HHMM).
L'heuristique est délibérément idiote — le nom de la branche est le sujet du travail, pas besoin de te redemander un nom.
Après installation je me suis demandé : je travaille parfois dans Amp — est-ce que ça se capture ? La réponse est exactement ce que la story du layering prédit :
| Scénario | Hook Claude Code | Hook git | Ce qui atterrit dans les notes |
|---|---|---|---|
| Travail Claude Code + commit | ✅ tire | ✅ tire | détail session + squelette commit |
| Travail Amp + commit | ❌ no-op | ✅ tire | squelette commit seul |
| Frappe manuelle + commit | ❌ no-op | ✅ tire | squelette commit seul |
| Travail Claude Code, pas encore commit | ✅ démarre l'enregistrement | — | détail session (l'entrée de commit attend le prochain commit) |
Conclusion : ça suffit si Claude Code est le conducteur principal. Le squelette de commit atterrit toujours ; le détail de session atterrit seulement sur le chemin Claude Code. Pour les articles « ce qui a été fait », on s'appuie surtout sur les bodies de commit — les prompts / traces bash de session sont un bonus, sympa à avoir mais pas indispensable.
Si tu utilises Amp intensivement, Amp a son propre système de hooks (je n'ai pas creusé les détails) ; un petit script de transfert déclenchant recording-state maybe-start/maybe-stop fonctionnerait de la même manière.
Porter le session recording de Claude Code vers un autre projet — les 5 gestes :
cp les deux scripts Python vers le bin/ du projet cible. Aucune modification — les scripts honorent $CLAUDE_PROJECT_DIR, portables entre projets par conception..claude/settings.local.json, hooks uniquement. Ne porte pas la liste de permissions — les permissions sont un état projet (différent par projet) ; les hooks sont des patterns (identiques entre projets)..git/hooks/post-commit (3 lignes), export CLAUDE_PROJECT_DIR=$ROOT manuellement et appeler recording-state commit. C'est le seul point où le monde git et le monde Claude Code sont pontés par la même variable d'env.docs/notes/ au .gitignore. Les notes sont transitoires + privées, pas partie du repo.CLAUDE_PROJECT_DIR=$(pwd) ./bin/recording-state maybe-start, vérifier que l'inférence branche→feature est correcte. Si le tree est propre, le script est conçu pour sauter — pas un bug.La vraie décision de conception n'est pas « comment je porte » — porter, c'est presque cp. C'est découper la logique d'enregistrement en 4 couches distinctes, chacune faisant une chose claire :
CLAUDE_PROJECT_DIRChacune des 4 peut être portée, remplacée ou sautée indépendamment (envie d'un support Amp ? ajoute une couche hook Amp. Changer le format de notes ? modifie le Python. Pas envie que git suive les notes ? vire le post-commit). Claude peut écrire le code correctement — mais le jugement sur « à quelle couche appartient cette fonctionnalité » ne peut pas être fait à ta place. C'est un appel sur tes frontières d'outillage, et il est à toi.