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 :
- Serveur HTTP en place dès le premier hook — l'overhead d'ajouter
processRequest()est nul et l'architecture est propre - Mesurer avant d'ajouter —
hook_http_firesethook_guard_eventsdonnent la fréquence réelle de chaque hook - Distinguer bloquant/async — les hooks PreToolUse bloquent Claude ; les PostToolUse ne bloquent pas. Prioriser la migration des PreToolUse
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.
Mon interface Claude Code ralentissait chaque jour — jusqu'à ce que j'audite mes hooks