Les hooks de Claude Code & Journaliser en append-only
Comprendre les hooks — des déclencheurs déterministes sur le cycle de vie de Claude Code — et s'en servir pour journaliser chaque action en append-only, une mémoire sur disque qui ne coûte pas un token.
Les hooks de Claude Code : déclencheurs sur les outils
L’idée en une phrase
Un hook est une commande shell que Claude Code exécute automatiquement à un point précis de son cycle de vie — avant un appel d’outil, après, à la soumission d’un prompt, avant une compaction — sans passer par le modèle ; c’est le levier le plus radical du framework R&D : ce qui est fait par un hook est délégué hors du LLM, donc coûte zéro token dans la fenêtre de l’agent, et ce qui n’a plus besoin d’être demandé en prompt est autant de contexte réduit.
Analogie : les capteurs d’une chaîne de montage. Chaque pièce qui passe est pesée, comptée, photographiée — sans que la chaîne ralentisse et sans qu’aucun opérateur n’y consacre une seconde d’attention. L’opérateur (l’agent) reste concentré sur son geste ; les capteurs (les hooks) garantissent, mécaniquement, que rien ne passe sans être vérifié ou enregistré.
Points clés
- Un hook s’accroche à un événement du cycle de vie. Les principaux :
PreToolUse(avant chaque appel d’outil — peut le bloquer),PostToolUse(après chaque appel réussi),UserPromptSubmit,Stop(fin de réponse),SessionStart/SessionEnd,PreCompact/PostCompact. Les événements d’outils se déclenchent à chaque appel de la boucle agentique, y compris dans les sous-agents. - La configuration vit dans
.claude/settings.json(niveau projet, versionnable — elle voyage avec le repo) ou~/.claude/settings.json(niveau utilisateur). Trois niveaux d’imbrication : l’événement, un matcher qui filtre (nom d’outil :Bash,Edit\|Write, regexmcp__.*), et les handlers à exécuter. - Le hook reçoit le contexte de l’événement en JSON sur stdin :
session_id,transcript_path,cwd,hook_event_name, et pour les outilstool_nameettool_input. Il répond par son code de sortie :0= silence (rien n’entre dans la fenêtre),2= blocage pourPreToolUse, et pourPostToolUsele stderr est montré à l’agent (l’outil a déjà tourné). - Un hook est déterministe, une instruction de prompt ne l’est pas. « Lance le linter après chaque édition » écrit dans le CLAUDE.md coûte ses tokens à chaque session et peut être oublié — précisément quand la fenêtre sature (lois d’échelle inverses, chapitre 2). Le hook, lui, tourne à tous les coups, saturation ou pas.
- Le champ
ifaffine le déclenchement avec la syntaxe des règles de permission (Bash(git *),Edit(*.ts)) — mais il est best-effort : pour interdire durement, c’est le système de permissions qu’il faut, pas un hook.
Exemple concret
Un projet impose de vérifier le formatage après chaque modification de fichier. Version prompt : une consigne de ~40 tokens dans le CLAUDE.md, rechargée à chaque session, appliquée par un agent qui doit y penser à chaque tour — et qui, vers ~70 % d’occupation de fenêtre, commence à l’oublier une fois sur cinq. Version hook : un PostToolUse avec matcher Edit\|Write qui lance le formateur. Coût dans la fenêtre : 0 token. Fiabilité : 100 % des éditions, du premier au dernier tour, dans la session principale comme dans les sous-agents. La règle est sortie du contexte pour entrer dans l’infrastructure — l’attention de l’agent reste entière pour la tâche.
Instruction de prompt vs hook — où placer une règle récurrente
| Critère | Instruction dans CLAUDE.md | Hook dans .claude/settings.json |
|---|---|---|
| Coût dans la fenêtre | ~40 tokens par session, à chaque requête | 0 token (exit 0 silencieux) |
| Fiabilité | dépend de l’attention — décroît quand la fenêtre se remplit | déterministe, à chaque événement |
| Portée | l’agent principal (les sous-agents ne lisent pas tout) | session principale et sous-agents |
| Position sur l’axe R&D | consomme du Reduce | Delegate hors-LLM + Reduce |
Config — un hook PostToolUse minimal
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npm run lint --silent"
}
]
}
]
}
}
# Le POURQUOI : la règle tourne hors du modèle, elle ne dépend plus de son attention.
# Le matcher "Edit|Write" ne déclenche que sur les modifications de fichiers ;
# exit 0 sans sortie = totalement invisible pour l'agent, 0 token ajouté.
Piège courant : « un hook consomme du contexte à chaque déclenchement » est inexact — un hook qui sort en
exit 0sans rien imprimer est invisible pour le modèle : il ne coûte pas un token, quel que soit le nombre de déclenchements. Seuls ajoutent du contexte les cas où le hook choisit de parler :exit 2(le stderr est montré à l’agent) ou les événements qui injectent du texte (UserPromptSubmit,SessionStart). Le silence est le mode par défaut — et c’est lui qui fait du hook un outil de context engineering.
Journaliser en append-only (PostToolUse)
L’idée en une phrase
Le journal append-only est un pattern de context engineering — pas une fonctionnalité native — où un hook PostToolUse ajoute une ligne à un fichier de log à chaque action de l’agent : la trace complète de la session vit alors sur disque et hors de la fenêtre, une mémoire qui coûte 0 token tant qu’on ne la relit pas — c’est du Reduce structurel, et la fondation des Context Bundles du chapitre suivant.
Analogie : la boîte noire d’un avion. Elle enregistre chaque paramètre du vol, en continu, sans jamais solliciter le pilote ni encombrer le cockpit d’un seul cadran de plus. Les trois quarts des vols, personne ne l’ouvre. Mais le jour où il faut reconstituer ce qui s’est passé, elle contient exactement la trajectoire — horodatée, ordonnée, complète.
Points clés
- Une action = une ligne, et on n’efface jamais. Le hook
PostToolUse(matcher*) reçoittool_nameettool_inputsur stdin et ajoute (>>) une ligne JSONL au journal de la session. Append-only : pas de réécriture, pas de verrou, pas d’état — chaque déclenchement est indépendant. - On journalise l’intention, pas le bruit. La ligne retient l’outil et sa cible (le fichier édité, la commande lancée) — quelques dizaines de tokens — jamais le résultat (contenu de fichier, sortie de build), qui pèserait des milliers de tokens et que le disque fournit déjà. Le tri exact entre ce qu’on garde et ce qu’on exclut est l’objet du chapitre suivant.
- Le journal vit hors de la fenêtre. Écrire dedans ne passe pas par le modèle : coût dans le contexte, 0 token, que la session compte 10 ou 500 actions. C’est la différence structurelle avec l’historique de conversation, qui repart à chaque requête (chapitre 10).
- Ce n’est pas le transcript. Claude Code écrit déjà un transcript complet (
transcript_path) — mais il contient tout, résultats d’outils compris : le relire coûterait autant que l’historique qu’il reflète. Le journal est une sélection : c’est elle qui le rend relisible à bas coût. async: truerend le hook non bloquant : l’écriture part en arrière-plan et la boucle agentique ne l’attend pas. Pour unecho >>local le gain est marginal, mais le réflexe compte dès que le handler fait plus qu’appender.
Exemple concret
Une session de refactorisation de 3 heures totalise ~200 appels d’outils. Le hook journalise chacun en une ligne de ~30 tokens : le journal complet pèse ~6 000 tokens — sur disque. Dans la fenêtre : 0. La session, elle, sature à ~140 000 tokens (~70 % d’une fenêtre de 200k, chapitre 10). Deux issues : compacter (une requête de ~140k tokens, résumé avec perte) — ou repartir de zéro par /clear et relire le journal : ~6 000 tokens, soit ~3 % de la fenêtre, qui restituent la trajectoire exacte — quels fichiers ont été touchés, dans quel ordre, quelles commandes ont tourné. La mémoire n’a rien coûté pendant trois heures et ne coûte que ce qu’on décide d’en relire.
Où vit la mémoire d’une session — trois supports
| Support | Coût dans la fenêtre | Survit à /clear ? | Relisible à bas coût ? |
|---|---|---|---|
| Historique de conversation | intégral, à chaque requête | non | — |
| Transcript de Claude Code | 0 tant qu’on ne le lit pas | oui | non — tout y est, résultats compris |
| Journal append-only | 0 tant qu’on ne le lit pas | oui | oui — ~30 tokens par action |
Config — brancher le journal dans .claude/settings.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/journal-session.sh",
"args": [],
"async": true
}
]
}
]
}
}
# Le POURQUOI : chaque action laisse une trace d'une ligne, hors de la fenêtre.
# ${CLAUDE_PROJECT_DIR} pointe la racine du projet quel que soit le cwd du moment ;
# "args": [] passe en forme exec (pas de shell intermédiaire, pas de quoting) ;
# "async": true écrit en arrière-plan sans retarder la boucle agentique.
Piège courant : « journaliser est inutile, le transcript de Claude Code enregistre déjà tout » est inexact — c’est justement parce qu’il enregistre tout que le transcript ne sert pas de mémoire de travail : relire un transcript de session saturée coûte l’équivalent de l’historique qu’il reflète (~140k tokens dans l’exemple ci-dessus). Le journal append-only vaut par ce qu’il exclut : ~6 000 tokens de trajectoire au lieu de ~140 000 de flux brut. Une mémoire utile n’est pas une mémoire exhaustive — c’est une mémoire sélective.
Fil rouge — Reduce ou Delegate ?
Les hooks jouent sur les deux tableaux de l’axe R&D. Côté Delegate, ils déportent les règles récurrentes hors du LLM lui-même : le linter, la vérification, l’enregistrement tournent en shell, à coût d’attention nul — la forme la plus pure de délégation, puisque le délégué n’a même pas de fenêtre de contexte. Côté Reduce, chaque règle sortie du CLAUDE.md rend ses tokens, et le journal append-only pousse la logique à son terme : ~200 actions mémorisées pour 0 token dans la fenêtre, contre ~140 000 si l’historique devait s’en souvenir seul. L’agent reste concentré sur la tâche ; l’infrastructure se souvient à sa place — le chapitre suivant organise cette trace en Context Bundles réutilisables.
Travaux pratiques
À chaque leçon, un petit artefact à déposer dans ton .claude/ — commande, sous-agent, hook, mémo… — pour te bâtir, au fil du livre, une boîte à outils de context engineering réutilisable.
Un hook maison — le journal de session append-only
L’artefact du jour est le pattern complet de la fiche : un hook PostToolUse qui trace chaque action de l’agent dans un journal JSONL, un fichier par session. C’est du Reduce structurel — la mémoire de la session vit sur disque pour 0 token dans la fenêtre — et la brique sur laquelle les chapitres 12 et 13 construiront les Context Bundles et le remounting. Deux fichiers : l’extrait de .claude/settings.json et le script.
{
"hooks": {
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/journal-session.sh",
"args": [],
"async": true
}
]
}
]
}
}
Le script, à enregistrer dans .claude/hooks/journal-session.sh puis rendre exécutable (chmod +x .claude/hooks/journal-session.sh) — il nécessite jq :
#!/bin/bash
# journal-session.sh — une ligne JSONL par action de l'agent, en append-only.
# POURQUOI : la trace vit sur disque, hors de la fenêtre — 0 token tant qu'on ne la relit pas.
INPUT=$(cat) # le JSON de l'événement arrive sur stdin
SESSION=$(echo "$INPUT" | jq -r '.session_id')
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
# On journalise l'INTENTION (outil + cible), jamais le résultat : c'est la
# sélection qui rendra le journal relisible à bas coût (~30 tokens la ligne).
TARGET=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.command // .tool_input.pattern // "-" | .[0:200]')
LOG_DIR="${CLAUDE_PROJECT_DIR:-.}/.claude/journal"
mkdir -p "$LOG_DIR"
# Append-only : un fichier par session, une ligne par action, on n'efface jamais.
jq -cn --arg ts "$(date -u +%FT%TZ)" --arg tool "$TOOL" --arg target "$TARGET" \
'{ts: $ts, tool: $tool, target: $target}' >> "$LOG_DIR/$SESSION.jsonl"
exit 0 # silence total : rien ne remonte dans la fenêtre de l'agent
Une fois les deux fichiers en place (et .claude/journal/ ajouté au .gitignore), chaque session laisse sa boîte noire : ~30 tokens par action, sur disque, 0 dans la fenêtre. Après un /clear, cat .claude/journal/<session>.jsonl restitue la trajectoire complète pour quelques milliers de tokens — le chapitre suivant apprendra à trier ce qui mérite d’y entrer.