Mon interface Claude Code ralentissait chaque jour — jusqu'à ce que j'audite mes hooks

Claude Code devenait de plus en plus lent. Ni les modèles, ni l'API, ni le réseau. La vraie cause : 75 hooks command qui spawneraient un process Bun à chaque action. Voici comment les hooks HTTP ont tout résolu.

Pendant des semaines, j'ai eu le sentiment que mon interface Claude Code ralentissait. Pas d'un coup. Progressivement. Un Edit qui prend une seconde de trop. Un Bash qui bloque avant de répondre. Une sensation diffuse d'attrition.

J'ai cherché côté modèle, côté API, côté réseau. Rien. Puis j'ai audité mes hooks — et j'ai trouvé.


Avant de continuer de lire la suite de l'article, je vous invite à vous inscrire à ma newsletter, pour connaître en avant première les futurs sujets traités chaque semaine.


Le symptôme : une lenteur qui s'accumule

Mon infra Claude Code a évolué sur plusieurs mois. J'ai ajouté des hooks progressivement : un pour sécuriser les commits Git, un pour traquer les tokens, un pour détecter les console.log, un pour injecter du contexte sémantique avant chaque tool call, un pour la conscience artificielle, un autre pour l'analyse de code...

À un moment donné, j'ai eu 75 hooks actifs.

Chaque hook de type command fonctionne de la même façon : Claude Code lance un nouveau process Bun, qui charge ses modules, s'initialise, lit stdin, traite, écrit stdout, puis se ferme. Un cold-start complet.

{
  "type": "command",
  "command": "/home/mathieu/.local/bin/bun /home/mathieu/.claude/scripts/hooks/semantic-context.ts"
}

Le coût de ce cold-start ? ~230ms par appel.

Sur un Edit d'un fichier .ts, voici ce qui se passait avant ma migration :

# Hook Mode Latence
1 semantic-context.ts sync (bloquant) 230ms
2 context-budget-tracker.ts sync 230ms
3 prettier sync (bloquant) 500–2000ms
4 console.log detect sync 200–500ms
5 grepai-ensure-watch sync 500–2000ms
6 semantic-duplicates.ts sync (bloquant) 2000–8000ms
11 autres hooks async variable

Latence sync totale par Edit .ts : 3,3 à 12,6 secondes.

Ce n'était pas le modèle. C'était mon infrastructure qui se dévorait elle-même.


La cause racine : le cold-start Bun à répétition

Chaque hook command lance un nouveau process. Bun est rapide au démarrage — mais pas gratuit. Sur un Mac ou un Linux rapide, un bun script.ts prend environ 230ms de cold-start. Sur WSL2 comme moi, c'est parfois plus.

Avec 45 hooks de type command actifs et plusieurs dizaines de tool calls par session, ça représente des dizaines de secondes de latence accumulée que je subissais sans en voir la cause.

Le pattern était invisible à l'œil nu : chaque hook isolément semblait raisonnable. C'est leur accumulation qui créait la friction.


La solution : un serveur HTTP persistant

L'idée est simple : au lieu de spawner un nouveau process Bun pour chaque hook, on lance un seul serveur Bun au démarrage de session — et tous les hooks HTTP font une requête HTTP locale vers ce serveur.

{
  "type": "http",
  "url": "http://127.0.0.1:18766/semantic-context"
}

Le serveur hooks-http-server.ts charge tous ses modules une seule fois au démarrage :

import { processRequest as processSemanticContext } from "./semantic-context";
import { processRequest as processTrackTokenUsage } from "./track-token-usage";
import { processRequest as processErrorWatchdog } from "./error-watchdog-append";
// ... 28 imports

Puis chaque requête est traitée in-process, dans l'event-loop Bun, sans fork, sans reload de modules, sans initialisation.


Les chiffres du benchmark (2026-03-24)

J'ai mesuré la latence de chaque endpoint du serveur HTTP (30 runs/endpoint, méthode curl -w "%{time_total}").

PreToolUse — hooks bloquants (les plus critiques)

Endpoint Latence p50 Latence p95 Latence p99
/semantic-context 0,2 ms 0,5 ms 0,9 ms
/context-budget 0,2 ms 0,6 ms 1,3 ms
/write-guard 0,2 ms 0,7 ms 1,2 ms
/secret-guard 0,3 ms 0,9 ms 1,1 ms

PostToolUse — fire-and-forget (async)

Endpoint Latence p50 Latence p95
/track-token-usage 0,2 ms 1,4 ms
/activity 0,3 ms 1,2 ms
/kg-spread 0,3 ms 1,9 ms
/workflow-completion 0,2 ms 1,0 ms

Comparaison directe

Mode Latence p50 Ratio
bun script.ts cold-start ~230 ms ×1 (référence)
Bun JIT warm (1er appel) ~23 ms ×10
Serveur HTTP p50 0,2 ms ×1 150

Les hooks HTTP sont 1 150 fois plus rapides que le cold-start bun.

Scalabilité sous charge

Appels parallèles Durée totale Débit
10 7 ms ~1 429 req/s
50 22 ms ~2 273 req/s
100 37 ms ~2 703 req/s

Le résultat concret

Après migration de 30 hooks vers HTTP :

Métrique Avant Après Gain
Latence sync par Edit .ts 3,3–12,6 s 0,12–0,3 s -97 %
Latence p50 par hook ~230 ms 0,2 ms ×1 150
Économie estimée/session ~3 795 ms ~4 secondes
Débit max ~4 req/s 2 703 req/s ×675

La session fonctionne maintenant à la vitesse que j'attendais depuis le début. Plus de micro-latences accumulées. Le contexte se charge immédiatement.


Architecture dual-mode : le détail technique

La contrainte principale des hooks HTTP : chaque script doit être importable sans effets de bord. La règle est simple — ajouter if (import.meta.main) dans chaque script avant tout code standalone :

// semantic-context.ts
export async function processRequest(input: HookInput): Promise<HookOutput> {
  // logique du hook
  return { additionalContext: "..." };
}

// Standalone mode (bun semantic-context.ts depuis terminal)
if (import.meta.main) {
  const input = JSON.parse(await Bun.stdin.text());
  const output = await processRequest(input);
  console.log(JSON.stringify(output));
  process.exit(0);
}

Sans cette garde, un process.exit(0) dans le catch stdin tuerait le serveur entier au premier appel.

Le serveur lui-même est lancé par session-launchers.ts au SessionStart, avec gestion propre du EADDRINUSE (si le serveur tourne déjà, le second process s'arrête silencieusement).


Quels hooks NE pas migrer en HTTP

Tous les hooks ne gagnent pas à être migrés. J'ai laissé en command :

Script Raison
output-redactor.ts Modifie tool_output — mécanisme updatedMCPToolOutput réservé aux hooks command
warm-up-ollama.ts ~15s de latence (API externe) — le gain HTTP est marginal
hook-sync-openproject.ts ~90s (API OpenProject) — idem
code-review-graph-sync.ts Timeout 8 000ms, process npx externe
twig-lint.ts, php-validate.ts Processus externes lents — le bottleneck n'est pas le cold-start Bun
Scripts SessionEnd Déjà asynchrones, longue durée normale

Règle simple : migrer en HTTP un hook qui se déclenche fréquemment (>10×/session), dont la logique est pure TypeScript, et dont la latence cible est <50ms. Laisser en command tout hook qui dépend d'un processus externe ou qui modifie tool_output.


Ce que j'aurais dû faire dès le départ

En rétrospective, l'erreur était d'ajouter des hooks sans mesurer leur coût cumulatif. Chaque hook isolément semblait raisonnable. Ensemble, ils transformaient chaque interaction en un ballet de cold-starts.

Si je devais reconstruire l'infra depuis zéro :

  1. Serveur HTTP en place dès le premier hook — l'overhead d'ajouter processRequest() est nul et l'architecture est propre
  2. Mesurer avant d'ajouterhook_http_fires et hook_guard_events donnent la fréquence réelle de chaque hook
  3. Distinguer bloquant/async — les hooks PreToolUse bloquent Claude ; les PostToolUse ne bloquent pas. Prioriser la migration des PreToolUse
  4. if (import.meta.main) systématique — pas négociable pour les hooks HTTP

La lenteur progressive que je ressentais n'était pas mystérieuse. C'était de l'ingénierie : 75 cold-starts accumulés, sans que personne ne s'en rende compte, jusqu'à l'audit.


Mathieu GRENIER — Consultant IA & Architecture technique

Articles liés :
- RTK : l'outil qui m'a révélé combien mes commandes gaspillaient de tokens
- Monitoring RAG et conscience d'optimisation
- Fusion d'appels fonction : tokens, latence et oublis


Qui suis-je ?

Je suis Mathieu GRENIER, CTO d'Easystrat une startup de Montpellier, en France. Je manage une équipe d'une dizaine d'ingénieurs (Graphistes, IA, frontend, backend, devOps, AWS) en remote depuis le Japon.

J'ai aussi mon activité de freelance, où je conseille des entrepreneurs dans leurs projets d'application.

Avec mon expérience personnelle de plus de 15 ans en ESN, j'ai pu travailler pour un large panel d'entreprises de différentes tailles. Ma compréhension des problèmes métiers est une de mes grandes forces et permet à mes clients de pouvoir se projeter plus facilement.

L'essentiel de mon travail consiste à canaliser l'énergie des entrepreneurs sur l'essence même de leur projet.

La technologie, les méthodes, le management sont le cœur de mes compétences.

Vous pouvez me faire confiance sur ces points là.

Si vous voulez me parler d'un de vos projets, n'hésitez pas à m'envoyer un email avec vos disponibilités à : contact@mathieugrenier.fr

Tous les articles de ce blog sont écrits par moi, même si je peux m'aider de l'IA pour illustrer mes propos. Mais jamais je ne fournis d'articles 100 % IA.

Mathieu Grenier 27 mars 2026
Partager cet articlE