Projets · outillage de session

Trois hooks et un cliquet : Session Optimizer, suivi dans son code

Trois greffons se posent sur trois points d’une session Claude Code. L’un lit votre invite avant que le travail ne commence et demande que les mots vagues soient liés à de vrais artefacts. L’un surveille la taille de la conversation et l’arrête deux fois, une pour réfléchir, une pour passer la main. L’un met sur une ligne de terminal les chiffres qu’il faudrait sinon deviner. Ils ne partagent ni processus ni base de données, seulement quelques fichiers sous votre répertoire personnel, et rien de ce qu’ils calculent ne quitte la machine.6

Hookstrois événements en tout : UserPromptSubmit1, SessionStart2, et Stop avec SubagentStop3
Ne vous bloque jamaisle gate sort toujours en zéro4 ; le guard sort en zéro sur toute erreur d’analyse ou d’entrée-sortie5
Ce qui est mesuréles tokens de contexte du fil principal seulement : entrée plus les deux champs de cache14
Seuilsavertissement vers 120k à 180k, dur vers 160k à 200k tokens selon la famille de modèle, et votre propre fichier l’emporte16
Ce qui est écritune ébauche de checkpoint, deux fichiers par session sous /tmp, et une ligne de registre192535
Ce qui sortrien. Aucun appel réseau dans les trois greffons6
Pièce 01 · les coutures

Trois greffons, trois moments d’une session

Aucun de ces greffons n’est un programme qu’on lance. Chacun s’enregistre sur un point que l’hôte émet déjà, fait un travail borné, et s’efface. Le refine gate lit une invite avant qu’elle n’atteigne le modèle.1 La statusline se réinstalle au démarrage de session et c’est l’hôte qui la redessine à intervalle court.2 Le context guard tourne à la fin de chaque tour, et une fois de plus quand un sous-agent se termine.3

La règle commune aux trois est que échouer en silence vaut mieux qu’interférer. Le gate le dit explicitement : un gate capable de bloquer votre invite est pire que pas de gate du tout.4 Le guard sort en zéro sur toute erreur d’analyse ou d’entrée-sortie, parce qu’un hook Stop qui coince une session est pire qu’un checkpoint manqué.5 Ce ne sont pas des promesses de README : c’est la première chose que disent ces fichiers.

DÉROULÉ DE LA SESSION SessionStart install.sh sync UNE FOIS, AU DÉBUT resynchronise l’installation, idempotent et silencieux si rien n’a changé UserPromptSubmit refine_gate.py À CHAQUE INVITE lit le texte de l’invite, injecte des consignes ou n’imprime rien du tout rendu de la statusline statusline-command.sh TOUTES LES ~10 s charge de l’hôte, git, registres locaux, puis lignes ajustées à la largeur SubagentStop subagent-tracker.py À CHAQUE SOUS-AGENT lit le transcript de l’enfant, insère par id d’agent, recalcule Stop stop-context-guard.py FIN DE CHAQUE TOUR lit la queue du transcript, la compare aux seuils de ce modèle CE QUI EST ÉCRIT, ET OÙ Ébauches de checkpoint ~/.claude/memories/checkpoints/ <session_id>.md · latest.md état git capturé, cinq sections laissées au modèle à remplir État par session /tmp/zetetic-subagents-<id>.json /tmp/zetetic-ctxguard-<id>.json dépense des enfants, et le plus haut seuil déjà déclenché Registre de coûts et cache registre costs.sh cache télémétrie, TTL 15 s une ligne par session, relue plutôt que recalculée writes Rien ici n’appelle le réseau. Chaque chiffre est calculé sur votre machine, depuis la charge de l’hôte, votre git et ces fichiers. Une invite qui ne correspond à rien ne coûte aucun contexte : sans correspondance, le gate n’imprime rien.
Où vit l’état. L’ébauche de checkpoint et sa copie latest.md vont sous votre répertoire personnel ; l’agrégat de dépense des enfants et le fichier de niveau du guard sont des fichiers par session sous /tmp, donc ils ne survivent pas à un redémarrage.1925 La politique de confidentialité est sans ambiguïté sur la frontière : aucun greffon n’effectue d’appel réseau.6 Une affirmation de ce même fichier ne tient pas à cette révision, et le dossier le dit plutôt que de la reprendre : refine-gate y est crédité d’un fichier d’état local, alors que son hook n’écrit rien du tout.7
Pièce 02 · le gate

Ce que le refine gate entend, et ce qu’il répond

« Le correctif précédent est toujours cassé, refais-le marcher comme avant. » Chaque mot de cette phrase désigne quelque chose que le modèle ne voit pas. Le gate reconnaît la forme du problème plutôt que son sujet : sept expressions régulières couvrant cinq classes de référence non liée, tirées d’erreurs de liaison réellement survenues et non imaginées.8

Son deuxième étage est le plus intéressant, parce qu’il est structurel. Une invite qui se lit comme une demande de travail mais ne contient ni chemin, ni nom de fichier, ni empreinte de commit, ni référence de ligne est une invite dont personne ne pourra vérifier le résultat.9 Le raisonnement est écrit dans le fichier : aucun gate ne peut énumérer tous les vocabulaires métier, alors il teste l’ancrage, et une invite que vous avez ancrée vous-même est laissée tranquille.10 Sur correspondance il injecte des consignes ; sans correspondance il n’imprime rien, donc le cas courant ne coûte aucun contexte.11

ENTRÉE DU HOOK l’invite brute payload["prompt"] délai 5 s Vide, ou commence par / ? une commande slash porte ses propres consignes ÉTAGE 1 Marqueurs de référence ? 7 regex, IGNORECASE tirées de vraies erreurs de liaison, pas inventées ÉTAGE 2 Demande sans ancre ? WORK et pas ANCHOR structurel, pas un vocabulaire de domaines INJECTÉ, ÉTAGE 1 1 · lier chaque référence 2 · symptôme, but, non-buts 3 · choisir une stratégie 4 · signaux d’acceptation externes INJECTÉ, ÉTAGE 2 1 · quel module EST « le système X » ? lier les noms 2 · rappeler les tentatives puis 3, 4, 5 comme ci-dessus Aucune sortie le flux de l’hôte est intact, et aucun contexte n’est dépensé LES CINQ CLASSES DE MARQUEURS raccourci vers un artefact antérieur solution nommée mais non située comparaison à un référent tu référence à un comportement exact repeat-failure CE QUI COMPTE COMME ANCRE a/path/with/slashes un fichier.ext d’une liste fixe un sha de commit, 7 à 40 hex une référence :ligne Ancrez l’invite vous-même et le gate se tait. Il ne réécrit jamais votre invite, et il sort toujours en zéro, quoi qu’il arrive. yes no match sans corresp. yes no
Il demande du travail, il ne le fait pas. Ce qui est injecté est une procédure : lier chaque référence à un fichier, un commit ou un processus avec sa preuve ; séparer le symptôme du but ; choisir une seule stratégie d’exécution plutôt que d’en empiler ; et définir l’acceptation comme un signal externe plutôt que le modèle relisant sa propre sortie.12 Deux de ces règles citent des mesures contre l’échafaudage, ce qui est inhabituellement réfutable pour une consigne. Un chiffre du même fichier ne tombe pas juste : le texte annonce quinze stratégies et la table en porte dix-sept.13
Pièce 03 · le cliquet

Deux seuils, et un cliquet qui ne tourne que dans un sens

À la fin de chaque tour, le guard lit la queue de votre transcript plutôt que le fichier entier, en remontant par blocs de 64 Kio sous un plafond dur de 4 Mio. Cette taille n’est pas un pifomètre : le fichier consigne la mesure dont elle vient, un transcript de 24,5 Mo dont le dernier enregistrement d’usage était à 7 591 octets de la fin.15 De cet enregistrement il somme trois champs en un seul chiffre, les tokens d’entrée plus les deux champs de cache, et le compare aux seuils du modèle sur lequel vous êtes.1416

Ce qui suit mérite d’être compris avant l’installation. Les deux seuils rendent le même verdict à l’hôte : block.17 L’avertissement n’est pas un conseil qu’on ignore : c’est une interruption unique qui écrit un checkpoint et demande une pause de réflexion, après quoi la session continue.20 Le seuil dur interrompt une fois encore et vous demande d’effacer le contexte et de reprendre.21 Ni l’un ni l’autre ne peut se déclencher deux fois, car le plus haut niveau atteint est mémorisé par session.18

À CHAQUE Stop Lire la queue du transcript pas de 64 Kio, plafond 4 Mio dimensionné sur un transcript mesuré de 24,5 Mo : le dernier usage était à 7 591 octets de la fin LE CHIFFRE COMPARÉ input_tokens + cache_creation_input_tokens + cache_read_input_tokens le fil principal seulement none sortie silencieuse, sans effet warn bloque une fois, écrit l’ébauche, puis le travail continue hard bloque une fois, finit le checkpoint, demande /clear L’ébauche est mécanique état git gratuit ; cinq sections laissées « to be filled » SEUILS DE REPLI fable 120k / 160k mythos 120k / 160k haiku 120k / 170k sonnet 180k / 200k opus 180k / 200k default 180k / 200k warn / hard, en tokens de contexte. Votre fichier ~/.claude/ctxguard- thresholds.json l’emporte sur cette table. ctx ≥ warn ctx ≥ hard LE CLIQUET NE TOURNE QUE DANS UN SENS Un niveau déjà déclenché ne se redéclenche pas : le plus haut niveau atteint est gardé dans un fichier par session, et un Stop déjà en continuation forcée sort aussitôt. Les deux franchissements rendent le même verdict à l’hôte, "decision": "block" ; l’avertissement est une pause de réflexion unique, pas une suggestion, et toute erreur de lecture sort en zéro plutôt que de bloquer la session.
L’ébauche est mécanique, et le dit. Le hook capture ce que git donne gratuitement et laisse cinq sections marquées « to be filled » : buts, références de fichiers, erreurs et correctifs, état courant, prochaines étapes.19 Les remplir est un travail sémantique, et le hook ne le fait pas : il demande au modèle de lancer un petit agent rédacteur, et se rabat sur l’écriture directe si cet agent n’est pas installé.22 Cet agent est tenu à 16K de contexte et interdit d’inventer quoi que ce soit d’absent de son entrée.24 La formulation « mémoire scopée » n’apparaît que si un outil de mémoire est réellement présent sur la machine.23 Un fichier sur le disque n’est donc pas encore la preuve d’une passation utile.
Pièce 04 · les chiffres

Trois mesures qu’il ne faut pas additionner

Un gros contexte courant et une grosse dépense cumulée sont deux faits différents. Le chiffre de contexte ne porte que sur le fil principal et exclut délibérément les enfants, parce que mêler leurs tokens déclencherait les checkpoints au mauvais moment ; la dépense des enfants est rapportée à côté.1425 L’agrégat qui la porte est indexé par identifiant d’agent, donc un événement répété met à jour une entrée au lieu de la doubler, et les transcripts frères sont balayés au cas où un événement aurait été manqué.26 Un étage plus bas, les tours d’assistant sont dédupliqués par identifiant de message, le plus grand usage l’emportant, parce que l’hôte réenregistre le même tour au fil des bifurcations et des reprises.27

La statusline applique ensuite une règle facile à énoncer et facile à rater : les comptes et les volumes de tokens viennent de l’agrégat, et chaque montant en dollars vient du seul registre, de sorte qu’une même dépense n’est jamais tarifée deux fois sur un même écran.313235 Quand le terminal est étroit, l’ajustement coupe par la queue et garde les segments de tête, car la charge de l’hôte ne porte aucun champ de largeur et la mise en page doit la deviner.2930

CE QUI EST LU CE QUI EST GARDÉ EN LOCAL CE QUI LE LIT Charge statusLine de l’hôte model · context_window rate_limits · cost · pr aucun champ de largeur Transcript principal <session_id>.jsonl tarifé une fois, puis relu plutôt que recalculée Transcripts des enfants subagents/agent-<id>.jsonl les frères sont balayés aussi, si un événement a été manqué Cache de télémétrie TTL 15 s, écrit en arrière- plan, sous verrou Registre de coûts costs.sh, une ligne par session la source unique de chaque montant affiché à l’écran Agrégat de dépense enfants /tmp/zetetic-subagents-<id>.json clé = id d’agent : un événement répété met à jour, ne double pas La statusline comptes et volumes de tokens de l’agrégat, chaque montant du registre. Jamais les deux. L’avis de checkpoint rapporte la dépense enfants à côté du contexte, et la tient hors de la décision de seuil le registre compte déjà les enfants TROIS MESURES, PAS UN SEUL CHIFFRE Un gros contexte courant et une grosse dépense cumulée sur les agents enfants sont des faits différents, et le code les tient séparés. Le coût d’un rendu complet de la statusline, en millisecondes, n’est mesuré nulle part dans les sources : seuls les chemins qui évitent du travail le sont.
Deux prix pour un même travail serait un défaut, pas une fonctionnalité. La règle voulant que chaque montant affiché vienne d’un registre unique est ce qui garde la statusline honnête quand une session a engendré une douzaine d’enfants.32 La télémétrie derrière les segments de débit et de cache se rafraîchit au plus une fois par rafraîchissement et demi, en arrière-plan, sous verrou.33

Ce que ces trois greffons ne prouvent pas

Le résultat de bout en bout est une demande mieux ancrée, des signaux de session visibles et un point de reprise écrit. Le code soutient ce flux ; il n’établit pas que les consignes ont été suivies, que l’implémentation qui a suivi était correcte, ni qu’un résumé a gardé tout ce qui comptait. Cela demande une relecture. Trois limites de plus méritent d’être connues : la reconnaissance est anglaise et syntaxique, ce n’est pas une compréhension de votre phrase8 ; les fichiers temporaires sont sous /tmp et ne survivent pas à un redémarrage25 ; et le coût d’un rendu complet de la statusline, en millisecondes, n’est mesuré nulle part dans les sources, qui ne documentent que les chemins évitant du travail.33 Un détail est un choix assumé et non un oubli : il n’y a aucun emoji, parce qu’un glyphe doit s’apprendre et que plusieurs cassent le budget de colonnes.34 Les lignes sont rendues avec l’identité en premier.28

1plugins/refine-gate/hooks/hooks.json:3 :3-14 — un seul événement, UserPromptSubmit, avec un délai de 5 s
2plugins/statusline/hooks/hooks.json:3 :3-18 — un seul événement, SessionStart filtré sur startup|resume, qui relance l’installeur au lieu de rendre quoi que ce soit
3plugins/context-guard/hooks/hooks.json:3 :3-24 — les deux seuls événements, Stop et SubagentStop, chacun avec un délai de 10 s
4plugins/refine-gate/hooks/refine_gate.py:15 :14-16 — « Always exits 0 — a gate that can block the user’s prompt is worse than no gate. »
5plugins/context-guard/hooks/stop-context-guard.py:52 :52 — un Stop déjà en continuation forcée sort aussitôt ; toute erreur d’analyse ou d’entrée-sortie sort en 0 plutôt que de bloquer la session
6PRIVACY.md:24 :22-26 — « Nothing. No plugin in this marketplace makes any network call, sends telemetry, or transmits any content… »
7PRIVACY.md:17 :17 prête à refine-gate « a small local state file used to rate-limit the gate ». À cette révision, refine_gate.py n’ouvre aucun fichier et n’écrit rien : ou bien ce fichier a disparu, ou bien il n’a jamais existé
8plugins/refine-gate/hooks/refine_gate.py:30 :30-50 — sept regex pour cinq libellés, en insensible à la casse ; :27-29 indique qu’elles viennent d’erreurs de liaison réellement observées, non d’une invention
9plugins/refine-gate/hooks/refine_gate.py:61 :61-76 — le vocabulaire de demande de travail, et les quatre choses qui comptent comme ancre : un chemin contenant une barre oblique, un nom de fichier à extension connue, un sha de commit de 7 à 40 caractères, ou une référence :ligne
10plugins/refine-gate/hooks/refine_gate.py:56 :56-60 — « The gate cannot enumerate every domain vocabulary, so the test is structural instead… If the user grounded the prompt themselves, the gate stays out of the way. »
11plugins/refine-gate/hooks/refine_gate.py:144 :141-151 — le bloc injecté suit le contrat UserPromptSubmit de l’hôte ; :139-140 n’imprime rien du tout sans correspondance
12plugins/refine-gate/skills/refine/SKILL.md:123 :123-170 — la table des stratégies et ses quatre règles de sélection, dont le fait qu’échafauder une tâche simple dégrade le résultat et que la preuve externe vaut mieux que le modèle se relisant
13plugins/refine-gate/skills/refine/SKILL.md:127 :127 annonce quinze stratégies ; la table en dessous porte dix-sept lignes. Rien dans les sources n’explique l’écart : ne pas lire « 15 » comme un décompte de ce qui est livré
14plugins/context-guard/hooks/stop-context-guard.py:173 :173-178 — input_tokens plus cache_creation_input_tokens plus cache_read_input_tokens, pris sur le dernier enregistrement d’usage
15plugins/context-guard/hooks/stop-context-guard.py:138 :138-149 — la lecture remonte par blocs de 64 Kio sous un plafond dur de 4 Mio, dimensionné contre un transcript mesuré de 24,5 Mo dont le dernier enregistrement d’usage était à 7 591 octets de la fin
16plugins/context-guard/hooks/stop-context-guard.py:85 :83-92 — la table de repli ; :80-82 préfère votre propre ~/.claude/ctxguard-thresholds.json, et :111-128 résout le modèle par sous-chaîne, avec retour au défaut si la paire n’est pas ordonnée
17plugins/context-guard/hooks/stop-context-guard.py:405 :405-418 — hard, puis warn, puis sortie silencieuse ; les deux franchissements rendent "decision": "block" en :434 et :444
18plugins/context-guard/hooks/stop-context-guard.py:131 :131 — l’ordre des niveaux. Le plus haut niveau atteint est gardé par session, et seule une montée agit
19plugins/context-guard/hooks/stop-context-guard.py:330 :330-351 — les cinq sections laissées en attente ; :302 est le répertoire où l’ébauche et sa copie latest.md sont écrites
20plugins/context-guard/hooks/checkpoint_protocol.py:49 :49-55 — « ⚠ CHECKPOINT THRESHOLD … This is a reflection pause, NOT the end of the session. »
21plugins/context-guard/hooks/checkpoint_protocol.py:112 :112-128 — le message de plafond, et les trois lignes littérales sur lesquelles la réponse doit finir avant qu’on demande d’effacer
22plugins/context-guard/hooks/checkpoint_protocol.py:66 :66-76 — la consigne demande au modèle de lancer le rédacteur, avec un repli si l’agent n’est pas installé. Le hook ne lance rien lui-même
23plugins/context-guard/hooks/checkpoint_protocol.py:31 :31-44 — la formulation « scopée » n’est émise que si un outil de mémoire est réellement présent, pour qu’un utilisateur extérieur ne lise jamais de verbes propres à un store
24plugins/context-guard/agents/memory-writer.md:9 :9 — un budget dur de 16K de contexte, et « You persist; you do not think up new content. »
25plugins/context-guard/hooks/subagent-tracker.py:114 :114 — l’agrégat est indexé par identifiant d’agent : un événement répété met à jour une entrée au lieu de la doubler ; :87 recalcule les totaux
26plugins/context-guard/hooks/subagent-tracker.py:9 :9-13 — la charge porte le chemin du transcript de l’enfant ; les transcripts frères sont balayés aussi, pour rattraper un événement manqué plus tôt
27plugins/context-guard/tools/subagent_usage.py:203 :203-207 — les tours d’assistant sont dédupliqués par message.id, le plus grand usage gagne, parce que l’hôte réenregistre le même tour au fil des bifurcations et des reprises
28plugins/statusline/assets/statusline-command.sh:163 :163-182 — l’ordre dans lequel les lignes sont rendues, l’identité d’abord
29plugins/statusline/assets/statusline-lib/fit.sh:131 :127-135 — la plus longue suite de segments entiers qui tient dans la largeur ; c’est la queue qui saute
30plugins/statusline/assets/statusline-lib/layout.sh:18 :18-22 — vérifié contre une charge réelle : elle ne porte aucun champ de largeur, d’où la sonde à quatre échelons en :47-57
31plugins/statusline/assets/statusline-lib/session_state.sh:125 :120-132 — le compte et le volume de tokens viennent du fichier d’agrégat
32plugins/statusline/assets/statusline-lib/render.sh:167 :167 — les montants des enfants ne sont pas répétés, ils sont déjà dans le chiffre de session
33plugins/statusline/assets/statusline-lib/session_state.sh:76 :76 — le cache de télémétrie se rafraîchit au plus une fois par rafraîchissement et demi, en arrière-plan et sous verrou
34plugins/statusline/assets/statusline-lib/render.sh:23 :23-28 — aucun emoji nulle part : un glyphe doit s’apprendre, et plusieurs s’affichent en double largeur et cassent le budget de colonnes
35plugins/statusline/assets/costs.sh:9 :9 — une ligne de registre par session, insérée de façon idempotente