Avancé Chapitre 23-24 / 13

Les Context Bundles & Ce qu'on journalise

Transformer le journal append-only en Context Bundle — un journal de travail indexé par session, construit par hooks dans Claude Code — et décider avec précision ce qui y entre et ce qui en reste exclu pour qu'un agent neuf puisse le relire pour quelques milliers de tokens.

Les Context Bundles : le journal de travail indexé par session

L’idée en une phrase

Un Context Bundle est un pattern de context engineering — pas une fonctionnalité native de Claude Code — qui organise la trace d’une session en un fichier autonome, indexé et relisible : un en-tête (session, date, branche, objectif), la suite des prompts de l’utilisateur et la suite des intentions de l’agent (fichiers lus, fichiers modifiés, commandes lancées) ; c’est du Reduce poussé à son terme : la mémoire de travail vit sur disque pour 0 token et se recharge, quand on le décide, pour ~3 % de la fenêtre au lieu des ~70 % qu’occupait la session d’origine.

Analogie : un cahier de laboratoire. Chaque expérience y ouvre une page datée avec son objectif ; on y note ce qu’on a fait — quel réactif, quelle mesure, dans quel ordre — jamais les paillasses entières ni le contenu des flacons. Un collègue qui reprend l’expérience le lendemain lit la page en cinq minutes et sait exactement où en est le travail. Le cahier n’est pas l’expérience : il en est la trajectoire, indexée pour être retrouvée.

Points clés

  • Un bundle = une session, un fichier. Le journal append-only du chapitre 11 traçait des actions ; le bundle y ajoute ce qui le rend réutilisable : un en-tête écrit à l’ouverture de la session (identifiant, date, cwd, branche git, mode de permission) et l’objectif — le premier prompt de l’utilisateur, qui vaut résumé de la session.
  • Trois hooks suffisent, tous configurés dans .claude/settings.json : SessionStart pour l’en-tête (le champ source distingue startup, clear, compact et resume), UserPromptSubmit pour les prompts (le champ prompt arrive sur stdin) et PostToolUse pour les intentions (tool_name + tool_input). Tous sortent en exit 0 sans rien écrire sur stdout — pour UserPromptSubmit et SessionStart, un stdout non vide serait injecté dans le contexte de l’agent.
  • Un index tient le catalogue. Un fichier index.jsonl reçoit une ligne par session (identifiant, date, objectif tronqué). C’est lui que le futur /loadbundle (chapitre 13) lira en premier : ~30 tokens par session pour choisir laquelle recharger, sans ouvrir aucun bundle.
  • Le bundle est indépendant du transcript. Claude Code écrit déjà un transcript (transcript_path), exhaustif et non relisible à bas coût (chapitre 11). Le bundle est une projection du transcript : la trajectoire sans les charges utiles. Il survit à /clear, à la compaction et à la fermeture du terminal.
  • Le bundle se relit par un agent neuf. Sa raison d’être est le remounting : après /clear, un agent vierge lit l’en-tête, les prompts et les intentions, puis rouvre lui-même les 3 ou 4 fichiers utiles. L’état mental récupéré n’est pas total — l’ordre de grandeur constaté est de 60 à 70 % — mais il coûte ~6k tokens au lieu de ~140k.

Exemple concret

Une session de correction de bug dure 3 heures : 12 prompts de l’utilisateur, ~200 appels d’outils, une fenêtre saturée à ~140 000 tokens (~70 % de 200k, chapitre 10). Le bundle produit par les hooks pèse : en-tête ~60 tokens, 12 prompts × ~80 tokens ≈ 1 000 tokens, 200 intentions × ~25 tokens ≈ 5 000 tokens — ~6 000 tokens au total, sur disque, pour un coût dans la fenêtre de 0 pendant les trois heures. À la quatrième heure, l’agent hallucine un nom de fonction (lois d’échelle inverses, chapitre 2). Plutôt qu’une compaction à ~140k tokens, on fait /clear et on demande à l’agent neuf de lire le bundle : ~6 000 tokens, soit ~3 % de la fenêtre. Il sait quel bug est visé, quels 45 fichiers ont été lus, lesquels ont été modifiés, quelles commandes de test ont tourné — et repart avec ~97 % de fenêtre libre. Le même travail avec le transcript aurait coûté ~140k tokens : le bundle restitue la trajectoire à un coût 20 fois moindre.

Journal append-only vs Context Bundle — ce que le bundle ajoute

CritèreJournal append-only (chapitre 11)Context Bundle
Événements captésPostToolUse seulSessionStart + UserPromptSubmit + PostToolUse
Contient l’objectif ?non — les actions sans le pourquoioui — l’en-tête et chaque prompt
Retrouvable sans l’ouvrir ?non — il faut lister le dossieroui — index.jsonl, ~30 tokens par session
Coût de relecture (session de ~200 actions)~5 000 tokens~6 000 tokens (+ en-tête et prompts)
Position sur l’axe R&DReduceReduce, préparant le remounting

Config — les trois hooks d’un Context Bundle

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|clear|compact",
        "hooks": [
          { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/context-bundle.sh", "args": [] }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/context-bundle.sh", "args": [] }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "*",
        "hooks": [
          { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/context-bundle.sh", "args": [], "async": true }
        ]
      }
    ]
  }
}
# Le POURQUOI : un seul script reçoit les trois événements et lit hook_event_name
# sur stdin pour savoir quoi appender. SessionStart n'est pas async : l'en-tête
# doit exister avant la première intention. Le matcher "startup|clear|compact"
# ouvre un en-tête à chaque nouveau contexte — pas à la reprise (resume), où le
# bundle existe déjà.

Piège courant : « un Context Bundle, c’est la sauvegarde de la session — il suffit de le relire pour retrouver l’agent d’avant » est inexact. Le bundle ne contient ni les résultats d’outils ni les raisonnements de l’agent : il restitue la trajectoire, pas l’état. C’est précisément ce qui le rend relisible pour ~6k tokens — et ce qui plafonne la récupération à 60-70 % de l’état mental. Les 30 % restants, l’agent neuf les reconstruit en rouvrant quelques fichiers ciblés : ~10k tokens supplémentaires, contre ~140k pour tout rejouer. Pour une sauvegarde intégrale, c’est --resume et le transcript — au prix de la fenêtre pleine.


Ce qu’on journalise, ce qu’on exclut volontairement

L’idée en une phrase

La valeur d’un bundle tient à sa sélection : on journalise l’intention (prompt, chemin de fichier, commande, sous-agent lancé) et on exclut volontairement les charges utiles (contenu des fichiers lus, sortie des commandes, résultats de recherche, réponses MCP) et le bruit (relectures répétées, actions internes des sous-agents, secrets) ; c’est un tri Reduce appliqué à la mémoire elle-même — un bundle exhaustif serait aussi inutilisable que le transcript qu’il remplace.

Analogie : la fiche d’un catalogue de bibliothèque. Elle donne le titre, l’auteur, la cote et trois lignes de résumé — jamais le texte du livre. C’est cette économie qui permet de parcourir mille fiches en une heure et de retrouver l’ouvrage en une minute. Une fiche qui reproduirait le livre entier ne serait plus un catalogue : ce serait une seconde bibliothèque, tout aussi lente à consulter que la première.

Points clés

  • La règle de tri : garder ce qui reste vrai après /clear, exclure ce que le disque fournit déjà. Le chemin d’un fichier lu reste une information ; son contenu, l’agent neuf peut le relire s’il en a besoin — et seulement s’il en a besoin. Un Read de 400 lignes vaut ~4 000 à 6 000 tokens en réponse ; sa trace dans le bundle en vaut ~15. Le ratio dépasse 300 pour 1.
  • On journalise le prompt en entier, tronqué à quelques centaines de caractères. Le prompt est la donnée la plus dense du bundle : ~80 tokens qui expliquent pourquoi les 20 actions suivantes ont eu lieu. Les prompts longs (collage d’une stack trace, d’un fichier) sont tronqués : au-delà de ~500 caractères, c’est une charge utile, pas une intention.
  • On exclut tool_response, systématiquement. Le hook PostToolUse le reçoit sur stdin, mais il ne doit jamais atterrir dans le bundle : sortie de build, contenu de fichier, résultats de Grep — des milliers de tokens que l’agent neuf ne doit pas payer d’avance.
  • On exclut les actions des sous-agents. Le JSON du hook porte agent_id quand l’événement vient d’un sous-agent : le bundle ignore ces lignes et ne conserve que l’appel Agent de l’agent principal (type + description). La trajectoire du délégué lui appartient ; celle de l’orchestrateur suffit au remounting (chapitre 8 : c’est le rapport de synthèse qui remonte, pas le bruit).
  • On dédoublonne et on masque. Le 3e Read du même fichier n’apprend rien : une ligne par fichier et par session suffit. Les commandes qui manipulent des secrets (export TOKEN=, chaînes ressemblant à des clés) sont masquées avant écriture — un bundle est un fichier texte que l’on relit, partage et parfois versionne.

Exemple concret

Sur les ~200 appels de la session précédente : 120 Read, 40 Edit, 30 Bash, 10 Grep. Avec tool_response inclus, le bundle pèserait ~120 × 4 000 + 30 × 1 500 + 10 × 2 000 ≈ 545 000 tokens — plus de deux fenêtres et demie, illisible. Avec les intentions seules : 200 × ~25 ≈ 5 000 tokens. Le dédoublonnage des Read (120 lectures pour 45 fichiers distincts) ramène les intentions à ~125 lignes, soit ~3 100 tokens. Ajoutés à l’en-tête et aux 12 prompts, le bundle tombe à ~4 200 tokens — ~2 % de la fenêtre — et reste complet : chaque fichier touché y figure, chaque commande de test aussi. Le tri a divisé la taille par ~130 sans perdre une information que l’agent neuf ne puisse retrouver sur disque.

Grille de tri — ce qui entre, ce qui reste dehors

DonnéeVerdictCoût dans le bundlePourquoi
Prompt de l’utilisateur (tronqué à ~500 car.)on garde~80 tokensl’intention — irremplaçable après /clear
Chemin d’un fichier lu ou modifiéon garde (dédoublonné)~15 tokensreste vrai ; le contenu se relit à la demande
Commande Bash (tronquée, secrets masqués)on garde~20 tokensdit ce qui a été testé ou lancé
Appel Agent (type + description)on garde~30 tokensla délégation, sans sa trajectoire interne
tool_response (contenu, sortie, résultats)on exclut0 — sinon ~2 000 à 6 000 chacunle disque le fournit déjà
Actions internes des sous-agents (agent_id présent)on exclut0bruit du délégué (chapitre 8)

Commande — mesurer ce qu’un bundle coûterait à relire

# Ordre de grandeur : ~4 caractères par token en français technique.
# Le POURQUOI : vérifier qu'un bundle reste sous ~5 % de la fenêtre (10k tokens
# sur 200k) avant de le confier à un agent neuf. Au-delà, le tri est insuffisant.
for f in .claude/bundles/*.jsonl; do
  printf '%-48s %6d tokens ~\n' "$(basename "$f")" $(( $(wc -c < "$f") / 4 ))
done

Piège courant : « plus le bundle est complet, mieux l’agent neuf s’y retrouvera » est inexact — au-delà de quelques milliers de tokens, le bundle reproduit dans la nouvelle fenêtre la saturation qu’il devait éviter, et l’agent neuf commence sa session avec l’attention déjà entamée (lois d’échelle inverses, chapitre 2). Le bundle exhaustif de l’exemple pèse ~545k tokens : il ne se relit pas du tout. Le bundle trié pèse ~4k : il se relit en une requête et laisse ~98 % de fenêtre au travail. Un bundle vaut par ce qu’il omet ; ce qu’il omet, le disque le garde.


Fil rouge — Reduce ou Delegate ?

Ce chapitre est du Reduce appliqué à la mémoire : le Context Bundle sort la trajectoire de la session hors de la fenêtre (0 token pendant le travail, via des hooks silencieux — le Delegate hors-LLM du chapitre 11) et la rend rechargeable à ~2-3 % de la fenêtre au lieu des ~70 % qu’elle occupait. Le second sous-thème est la condition de ce gain : sans tri, le bundle pèserait ~545k tokens et ne vaudrait pas mieux que le transcript ; avec le tri (intentions seules, tool_response exclu, sous-agents ignorés, lectures dédoublonnées), il tombe à ~4k tokens. L’agent neuf qui le relit démarre avec ~98 % de fenêtre libre — donc une attention entière — et retrouve 60 à 70 % de l’état mental de son prédécesseur pour un coût 30 fois moindre qu’une compaction. Le chapitre suivant construit /loadbundle, la commande qui fait ce remounting.


Quiz — teste tes connaissances
Avancé 7 questions Objectif : 5/7 minimum
0/7
bonnes reponses
Objectif non atteint (minimum 5/7 requis).
Remonte relire la fiche memo en pretant attention aux points manques, puis cliquer sur « Recommencer » pour retenter.

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 — context-bundle.sh, le Context Bundle complet

L’artefact du jour remplace le journal du chapitre 11 par le bundle complet de la fiche : un seul script branché sur trois événements, qui écrit un fichier par session dans .claude/bundles/ et tient l’index. Il applique la grille de tri intégralement — intentions seules, tool_response jamais écrit, sous-agents ignorés, Read dédoublonnés, secrets masqués, prompts tronqués. C’est du Reduce : ~4k tokens de mémoire relisible sur disque, 0 dans la fenêtre. Le chapitre 13 y branchera /loadbundle. Deux fichiers : l’extrait de .claude/settings.json, puis le script (il nécessite jq).

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|clear|compact",
        "hooks": [
          { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/context-bundle.sh", "args": [] }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/context-bundle.sh", "args": [] }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "*",
        "hooks": [
          { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/context-bundle.sh", "args": [], "async": true }
        ]
      }
    ]
  }
}

Le script, à enregistrer dans .claude/hooks/context-bundle.sh puis rendre exécutable (chmod +x .claude/hooks/context-bundle.sh) :

#!/bin/bash
# context-bundle.sh — un Context Bundle par session : en-tête, prompts, intentions.
# POURQUOI : la trajectoire vit sur disque (0 token dans la fenêtre) et se relit
# pour ~2-3 % de la fenêtre. Tout ce qui est exclu ici, le disque le garde déjà.

INPUT=$(cat)  # le JSON de l'événement arrive sur stdin

# Les actions des sous-agents sont du bruit pour le remounting : on les ignore.
[ "$(echo "$INPUT" | jq -r '.agent_id // empty')" ] && exit 0

EVENT=$(echo "$INPUT" | jq -r '.hook_event_name')
SESSION=$(echo "$INPUT" | jq -r '.session_id')
DIR="${CLAUDE_PROJECT_DIR:-.}/.claude/bundles"
BUNDLE="$DIR/$SESSION.jsonl"
TS=$(date -u +%FT%TZ)
mkdir -p "$DIR"

case "$EVENT" in
  SessionStart)
    # L'en-tête : ce qu'un agent neuf doit savoir avant de lire la trajectoire.
    jq -cn --arg ts "$TS" --arg session "$SESSION" \
      --arg source "$(echo "$INPUT" | jq -r '.source')" \
      --arg cwd "$(echo "$INPUT" | jq -r '.cwd')" \
      --arg branch "$(git -C "$(echo "$INPUT" | jq -r '.cwd')" branch --show-current 2>/dev/null)" \
      '{type: "session", ts: $ts, session: $session, source: $source, cwd: $cwd, branch: $branch}' \
      >> "$BUNDLE"
    ;;

  UserPromptSubmit)
    # Le prompt entier, tronqué : au-delà de ~500 caractères c'est une charge utile.
    PROMPT=$(echo "$INPUT" | jq -r '.prompt | .[0:500]')
    jq -cn --arg ts "$TS" --arg prompt "$PROMPT" '{type: "prompt", ts: $ts, prompt: $prompt}' >> "$BUNDLE"
    # Le premier prompt de la session devient l'objectif dans l'index (~30 tokens).
    if [ "$(grep -c '"type":"prompt"' "$BUNDLE")" -eq 1 ]; then
      jq -cn --arg ts "$TS" --arg session "$SESSION" --arg objective "${PROMPT:0:120}" \
        '{ts: $ts, session: $session, objective: $objective}' >> "$DIR/index.jsonl"
    fi
    ;;

  PostToolUse)
    TOOL=$(echo "$INPUT" | jq -r '.tool_name')
    # L'intention (outil + cible), jamais tool_response. Pour Agent : type + description.
    TARGET=$(echo "$INPUT" | jq -r '
      .tool_input
      | (.file_path // .command // .pattern
         // (if .subagent_type then "\(.subagent_type): \(.description // "")" else null end)
         // "-")
      | .[0:200]')
    # Secrets masqués : un bundle est un fichier texte qu'on relit et qu'on partage.
    TARGET=$(echo "$TARGET" | sed -E 's/((TOKEN|SECRET|KEY|PASSWORD)[A-Z_]*=)[^ ]+/\1***/g')
    # Read dédoublonné : la 3e lecture du même fichier n'apprend rien à l'agent neuf.
    if [ "$TOOL" = "Read" ] && grep -qF "\"tool\":\"Read\",\"target\":$(jq -cn --arg t "$TARGET" '$t')" "$BUNDLE" 2>/dev/null; then
      exit 0
    fi
    jq -cn --arg ts "$TS" --arg tool "$TOOL" --arg target "$TARGET" \
      '{type: "action", ts: $ts, tool: $tool, target: $target}' >> "$BUNDLE"
    ;;
esac

exit 0  # silence total : rien sur stdout, rien n'entre dans la fenêtre de l'agent

Une fois les deux fichiers en place (et .claude/bundles/ ajouté au .gitignore), chaque session laisse un bundle d’environ 4k tokens pour ~200 actions — 0 token dans la fenêtre pendant le travail. Après un /clear, cat .claude/bundles/index.jsonl donne la liste des sessions pour ~30 tokens chacune, et cat .claude/bundles/<session>.jsonl restitue la trajectoire complète pour ~2 % de la fenêtre : c’est la matière première du remounting du chapitre suivant.