Prix de Claude Code : diviser la facture avec hooks et skills

Spotify annonce 90 % de tokens de lecture en moins grâce à un hook PreToolUse et à des skills. J'ai reproduit le montage sans leur plateforme, sur un fichier PHP de 8 861 lignes.

Prix de Claude Code : diviser la facture avec hooks et skills
Réponse rapide

Un hook PreToolUse refuse les lectures de fichiers au-delà d'un seuil de lignes et renvoie l'agent vers un skill qui fait résumer le fichier par un modèle moins cher. Sur un fichier de 8 861 lignes et 305 215 octets, le contexte de l'agent principal encaisse 1 595 octets au lieu de 305 215, soit un rapport de 191 pour 1. Chez Spotify, le même montage fait tomber d'environ 90 % les tokens de lecture d'un monorepo Java.

Short

Pourquoi ta facture Claude Code explose

Regarder le Short

La chaîne YouTube Gekkode publie d’autres vidéos sur l’IA et le code.

S’abonner pour soutenir la chaîne

Une lecture de fichier n’a l’air de rien tant qu’on ne la compte pas : le wp-includes/post.php d’une installation WordPress pèse 8 861 lignes et 305 215 octets, soit de l’ordre de 87 000 tokens qui entrent dans le contexte et y restent jusqu’au prochain /clear. Spotify a publié le 3 septembre 2026 un billet expliquant comment ses équipes ont fait tomber cette dépense d’environ 90 %, et le mécanisme se reproduit sans leur plateforme.

Ce que Spotify a réellement mesuré

Portal by Spotify est la distribution commerciale de Backstage. Le billet, signé Dimitri Mazmanov, y décrit des modes : des agents déclaratifs qui tournent sur un runtime éphémère, avec leur propre modèle et leurs propres outils MCP attachés. Deux modes portent l’essentiel du gain : bulk-reader, qui répond à une question en lisant plusieurs fichiers, et code-writer, qui produit du code répétitif à partir de motifs existants. Les exemples de l’article les font tourner sur Gemini 2.5 Flash.

Côté Claude Code, c’est le plugin shunt du dépôt public spotify/portal-ai-plugins (Apache-2.0) qui impose le détour. Son fichier de documentation décrit trois couches : des hooks PreToolUse, check-file-size et check-bash-read, qui bloquent les lectures au-delà de 350 lignes, seuil réglable par la variable d’environnement SHUNT_MIN_LINES, des scripts bash qui appellent la CLI Portal, des skills Markdown qui expliquent à l’agent quand s’en servir. L’économie annoncée tourne autour de 90 % sur la lecture en masse d’un monorepo Java.

Le chiffre qui pique est ailleurs dans le billet : un quart des responsables d’ingénierie interrogés dépenseraient déjà 200 à 500 dollars par développeur et par mois en tokens, certains au-delà de 2 000. Sur Hacker News, le fil ouvert le 4 septembre (269 points et 173 commentaires quand je l’ai lu, le 7) ne conteste pas le principe mais son périmètre : un commentateur rapporte qu’un modèle bon marché a laissé passer un défaut de concurrence subtil, plusieurs rappellent que l’agent principal finit souvent par relire les fichiers lui-même, ce qui annule le gain. La règle qui revient le plus souvent tient en une phrase : le petit modèle a le droit de désigner, pas de décider.

Pourquoi une lecture de fichier coûte-t-elle si cher ?

La version 2.1.257 de Claude Code, publiée le 1er septembre 2026, a fait de Claude Fable 5.1 le modèle par défaut : contexte d’un million de tokens, 10 $ par million de tokens en entrée, 50 $ en sortie, 0,25 $ le million en lecture de cache. Reprenons le fichier du début.

Opération sur post.php Tokens Coût, tarif Fable 5.1
Lecture intégrale par Read ≈ 87 000 en entrée 0,87 $
Le même contenu relu depuis le cache ≈ 87 000 en lecture de cache 0,02 $
Réponse d’un modèle ouvrier (1 595 octets) ≈ 456 en entrée 0,005 $

Ces 87 000 tokens ne se paient pas une fois. Ils sont renvoyés à chaque tour suivant. Tant que le cache tient, ils repassent à 0,02 $ le tour, dès qu’il est froid, la note repart à 0,87 $. La documentation d’Anthropic le dit sans détour : une question d’une ligne posée dans une session ouverte depuis la matinée tire de l’usage pour toute la conversation. Elle donne aussi les ordres de grandeur observés en entreprise, environ 13 $ par développeur et par jour actif, 150 à 250 $ par mois, avec 90 % des utilisateurs sous 30 $ par jour.

Écrire le hook PreToolUse

Un hook PreToolUse reçoit sur son entrée standard le JSON de l’appel d’outil, tool_name, tool_input, cwd, permission_mode, et rend sa décision sur sa sortie standard. Trois issues : un code de sortie 0 avec {} laisse le flux de permissions habituel décider, un code 0 avec un objet hookSpecificOutput tranche, un code 2 bloque quoi qu’il arrive, avec le message pris sur stderr. Pour ce montage, c’est la deuxième forme qu’il faut : refuser et expliquer où aller.

Le hook se déclare dans le settings.json du projet, donc versionnable avec le dépôt :

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read|Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/bulk-read-guard.sh",
            "timeout": 10,
            "statusMessage": "Contrôle de la taille du fichier…"
          }
        ]
      }
    ]
  }
}

Et voici le script, dans la version que j’ai exécutée :

bash
#!/usr/bin/env bash
# bulk-read-guard.sh — hook PreToolUse.
# Entrée : le JSON du hook sur stdin. Sortie : JSON sur stdout, code de sortie 0.
set -uo pipefail
set -f

SEUIL="${SHUNT_MIN_LINES:-350}"
laisser_passer() { echo '{}'; exit 0; }
est_entier() { case "$1" in ''|*[!0-9]*) return 1 ;; *) return 0 ;; esac; }

charge=$(cat)
outil=$(printf '%s' "$charge" | jq -r '.tool_name // empty')
fichier=""

case "$outil" in
  Read)
    fichier=$(printf '%s' "$charge" | jq -r '.tool_input.file_path // empty')
    limite=$(printf '%s' "$charge" | jq -r '.tool_input.limit // empty')
    # Une lecture déjà bornée sous le seuil ne coûte presque rien.
    if est_entier "$limite" && [ "$limite" -le "$SEUIL" ]; then laisser_passer; fi
    ;;
  Bash)
    commande=$(printf '%s' "$charge" | jq -r '.tool_input.command // empty')
    printf '%s' "$commande" | grep -Eq '^[[:space:]]*(cat|head|tail)([[:space:]]|$)' || laisser_passer
    # head -n 40 et tail -20 restent bon marché.
    borne=$(printf '%s' "$commande" \
      | sed -nE "s/.*-n[[:space:]]*([0-9]+).*/\1/p;s/.*[[:space:]]-([0-9]+).*/\1/p" | head -1)
    if est_entier "$borne" && [ "$borne" -le "$SEUIL" ]; then laisser_passer; fi
    for jeton in $commande; do
      case "$jeton" in -*) continue ;; esac
      if [ -f "$jeton" ]; then fichier="$jeton"; break; fi
    done
    ;;
  *) laisser_passer ;;
esac

[ -n "$fichier" ] && [ -f "$fichier" ] || laisser_passer
lignes=$(wc -l < "$fichier" | tr -d ' ')
[ "$lignes" -gt "$SEUIL" ] || laisser_passer

jq -n --arg f "$fichier" --arg l "$lignes" --arg s "$SEUIL" '{
  hookSpecificOutput: {
    hookEventName: "PreToolUse",
    permissionDecision: "deny",
    permissionDecisionReason: ($f + " fait " + $l + " lignes, au-dessus du seuil de " + $s
      + ". Passez par le skill bulk-read : "
      + ".claude/skills/bulk-read/scripts/resume-fichier.sh " + $f + " \"votre question\"."
      + " Lecture directe autorisée uniquement avec un limit sous le seuil.")
  },
  systemMessage: ("bulk-read : " + $f + " (" + $l + " lignes) dévié vers le modèle bon marché.")
}'
exit 0

Trois détails comptent. Le matcher couvre Read et Bash, sans quoi l’agent contourne le refus avec un cat. Une lecture déjà bornée par un limit sous le seuil passe sans discussion, sinon le hook renvoie l’agent vers le skill en boucle. Et le permissionDecisionReason s’adresse à l’agent, pas à vous, c’est le texte qu’il lit pour décider de la suite, donc il doit nommer le chemin exact du script à lancer. Voici ce que le hook renvoie sur un refus :

json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "wp-includes/post.php fait 8861 lignes, au-dessus du seuil de 350. Passez par le skill bulk-read : .claude/skills/bulk-read/scripts/resume-fichier.sh wp-includes/post.php \"votre question\". Lecture directe autorisée uniquement avec un limit sous le seuil."
  },
  "systemMessage": "bulk-read : wp-includes/post.php (8861 lignes) dévié vers le modèle bon marché."
}

Le skill derrière le hook

Le hook sait dire non, il ne sait pas travailler. Le skill s’en charge, et seules ses métadonnées, nom et description, occupent le contexte tant qu’il n’est pas déclenché, là où les mêmes consignes placées dans un CLAUDE.md sont chargées à chaque session. Le format est celui du standard décrit dans notre guide des Agent Skills : un dossier, un SKILL.md, des scripts à côté.

markdown
---
name: bulk-read
description: Lire, inventorier ou résumer un fichier de plus de 350 lignes sans le charger dans le contexte. À utiliser dès qu'un hook refuse une lecture pour cause de taille, avant d'ouvrir un gros fichier PHP, un journal d'erreurs ou un dump SQL.
---

# Lecture déportée vers un modèle bon marché

Le hook `bulk-read-guard.sh` refuse les lectures intégrales au-delà de 350 lignes.
Ne contournez pas le refus avec `cat` : la même règle s'applique à Bash.

## Marche à suivre

1. Formulez une question précise. « Où la validation du panier est-elle faite ? » coûte
   moins cher que « résume ce fichier ».
2. Lancez le script :

   ```
   .claude/skills/bulk-read/scripts/resume-fichier.sh <chemin> "<question>"
   ```

3. Le script envoie le fichier entier au modèle bon marché et ne renvoie que sa réponse,
   avec des numéros de ligne.
4. Relisez ensuite les seules zones utiles, avec `Read` et un `limit` sous le seuil :

   ```
   Read(file_path: "<chemin>", offset: 412, limit: 60)
   ```

## Quand ne pas s'en servir

- Fichier de moins de 350 lignes : lisez-le directement.
- Refactorisation qui doit modifier le fichier : il faut le texte exact, pas un résumé.
- Fichier contenant des secrets : le script l'envoie à un autre modèle.

Le script appelé par le skill tient en une douzaine de lignes. La commande du modèle ouvrier passe par une variable, ce qui permet de la remplacer pendant les essais :

bash
#!/usr/bin/env bash
# resume-fichier.sh — envoie un fichier entier à un modèle bon marché et ne renvoie que la réponse.
# Le fichier ne traverse jamais le contexte de l'agent principal.
set -uo pipefail
fichier="${1:?usage : resume-fichier.sh <chemin> [question]}"
question="${2:-Inventaire des classes, fonctions et points d entree, avec numeros de ligne}"
# En production : claude -p --model haiku. Pendant le test : SHUNT_CMD=./faux-modele.sh
: "${SHUNT_CMD:=claude -p --model haiku}"

{
  printf 'Question : %s\n' "$question"
  printf 'Reponds en 40 lignes maximum, en citant les numeros de ligne. Ne recopie pas le fichier.\n\n'
  printf -- '--- %s ---\n' "$fichier"
  cat -- "$fichier"
} | $SHUNT_CMD

Ce qu’a donné le banc d’essai

Un hook se teste sans agent : il lit du JSON sur son entrée standard et rend sa décision sur sa sortie. J’ai donc écrit un banc d’essai qui fabrique les charges utiles décrites dans la documentation des hooks, les envoie au script et compare la décision rendue à celle attendue. Dix cas, dix conformités :

bash
$ bash test-hook.sh
Seuil : 350 lignes
Cible : wp-includes/post.php (8861 lignes)

OK   01-read-gros-fichier       decision=deny  code=0
OK   02-read-limit-120          decision=allow code=0
OK   03-read-limit-2000         decision=deny  code=0
OK   04-read-petit-fichier      decision=allow code=0
OK   05-bash-cat-gros           decision=deny  code=0
OK   06-bash-head-40            decision=allow code=0
OK   07-bash-grep               decision=allow code=0
OK   08-read-fichier-absent     decision=allow code=0
OK   09-bash-sed-plage          decision=allow code=0
OK   10-edit-ignore             decision=allow code=0

Le cas 09 est le plus instructif : sed -n '1,4000p' passe. Le hook ne connaît que cat, head et tail, et n’importe quelle autre commande de lecture le traverse. Un hook réduit une dépense moyenne, il ne ferme pas une porte.

Reste la partie modèle. J’ai branché la variable SHUNT_CMD sur codex exec avec GPT-6 Astra, celui du comparatif Astra contre Fable 5.1, qui n’est pas un modèle bon marché, mais qui mesure la plomberie de bout en bout :

bash
$ SHUNT_CMD="codex exec --skip-git-repo-check --sandbox read-only -m gpt-6-astra -" \
    ./.claude/skills/bulk-read/scripts/resume-fichier.sh wp-includes/post.php \
    "Ou est faite la verification des capacites (current_user_can) dans ce fichier ?"

code=0 duree=36s
tokens used 91 805          # facturés au modèle ouvrier
$ wc -lc < resultats/resume-codex.txt
      10    1595            # ce qui revient à l'agent principal

Trente-six secondes, 91 805 tokens facturés au modèle ouvrier, et 1 595 octets qui reviennent à l’agent principal : un rapport de 191 pour 1 sur ce que le contexte encaisse. La réponse a été vérifiée à la main, au grep : six appels à current_user_can(), lignes 3436, 3489, 4734, 4736, 5105 et 7705, les six numéros sont exacts. Avec un substitut déterministe, un simple grep des déclarations à la place du modèle, le même montage rend 3 460 octets en 0,56 seconde : quand la question est structurelle, l’ouvrier n’a pas toujours besoin d’être un modèle.

Combien coûte Claude Code, au juste ?

À l’API, tout se lit dans la grille rappelée plus haut. À l’abonnement, la question devient celle des limites. Le 31 août 2026, la promotion « +50 % » sur les limites hebdomadaires a pris fin, remplacée par un relèvement permanent de 25 % de la limite de base, soit, pour qui profitait de la promotion, environ 17 % de moins qu’auparavant, ce qui a occupé un fil de Hacker News le jour même. Dans les deux cas, la commande /usage est le point de départ : elle affiche le coût de la session, la répartition par modèle, et depuis la version 2.1.251 une ligne « Prompt cache » qui donne la part des tokens d’entrée servis par le cache et le nombre de défauts. Sur un abonnement, elle ajoute la part de consommation attribuée aux skills, aux sous-agents, aux plugins et à chaque serveur MCP.

Les leviers à activer avant d’installer quoi que ce soit

  • Le cache de prompt. Il dure une heure sur abonnement, cinq minutes sur des crédits d’usage ou une clé d’API. Si la ligne « Prompt cache » de /usage annonce des défauts, cherchez la cause avant toute autre optimisation : une définition d’outil qui bouge en cours de session suffit à tout réécrire.
  • Les sous-agents. La sortie verbeuse d’une suite de tests ou d’un journal reste dans leur contexte, seul le résumé remonte. Pour les tâches simples, la documentation recommande model: haiku dans la configuration du sous-agent : c’est la version intégrée de l’idée de Spotify, sans plateforme à installer.
  • Le ménage dans les skills. Un skill chargé mais jamais invoqué paie quand même ses métadonnées à chaque session. La commande /skill-doctor, arrivée avec la version 2.1.261 du 4 septembre, liste précisément ceux-là et leur coût en contexte, nous lui consacrons un article entier.

Spotify n’a d’ailleurs rien inventé sur ce point. La documentation des coûts donne son propre exemple de hook de filtrage, un PreToolUse qui réécrit la commande de test pour ne remonter que les échecs, en utilisant le champ updatedInput plutôt qu’un refus. Réécrire plutôt que refuser est souvent plus doux, l’agent ne perd pas son tour.

Ce qu’il faut retenir

  • Le montage tient en deux fichiers : un hook PreToolUse qui refuse au-delà d’un seuil de lignes, un skill qui fait faire le travail ailleurs.
  • Le refus doit passer par permissionDecision: "deny" et un permissionDecisionReason qui nomme le script de rechange, pas par un code de sortie 2.
  • Sur un fichier de 8 861 lignes, l’agent principal encaisse 1 595 octets au lieu de 305 215, soit 191 pour 1.
  • Couvrez Bash autant que Read, et laissez passer les lectures déjà bornées par un limit.
  • Avant d’installer quoi que ce soit : /usage, le cache de prompt, les sous-agents en modèle bon marché.

Erreurs fréquentes

Bloquer Read et oublier Bash L'agent contourne le refus avec cat. Le matcher doit couvrir Read|Bash et le script inspecter .tool_input.command.
Refuser une lecture déjà bornée Un Read avec un limit sous le seuil ne coûte presque rien. Sans ce test, le hook renvoie l'agent vers le skill en boucle et vous payez le détour pour rien.
Sortir en code 2 pour bloquer Le code 2 bloque quoi qu'il arrive et le message vient de stderr. Pour dire à l'agent quoi faire à la place, sortez en code 0 avec permissionDecision: deny et un permissionDecisionReason qui nomme le script.
Croire que le hook ferme la porte sed -n '1,4000p' passe, comme toute commande de lecture non prévue. Un hook fait baisser une moyenne, il ne garantit rien.
Faire résumer un fichier sensible Le script envoie le fichier entier à un autre modèle, souvent chez un autre fournisseur. Excluez les .env, les dumps et tout ce qui porte des identifiants.

Claude CodeMCPPerformanceSkills

Damien Flandrin Développeur web depuis 2010, créateur de Gekkode et d’Email Impact. Chaque article est testé sur un projet réel avant publication. Contact
Newsletter

Les nouveaux tests, tutoriels et projets, par e-mail.

Tests reproductibles, code versionné, résultats datés. Jamais de spam.