Claude Code-kosten verlagen met hooks en skills

Spotify claimt 90% minder leestokens dankzij een hook PreToolUse en skills. Ik heb de opzet zonder hun platform nagebouwd, op een PHP-bestand van 8.861 regels.

Claude Code-kosten verlagen met hooks en skills
Kort antwoord

Een hook PreToolUse weigert bestandslezingen boven een regeldrempel en stuurt de agent naar een skill die het bestand door een goedkoper model laat samenvatten. Op een bestand van 8.861 regels en 305.215 bytes krijgt de context van de hoofdagent 1.595 bytes te verwerken in plaats van 305.215, oftewel een verhouding van 191 op 1. Bij Spotify brengt dezelfde opzet de leestokens van een Java-monorepo met ongeveer 90% omlaag.

Een bestand lezen lijkt onschuldig zolang je het niet natelt: wp-includes/post.php van een WordPress-installatie weegt 8.861 regels en 305.215 bytes, oftewel zo’n 87.000 tokens die de context in gaan en daar blijven tot de volgende /clear. Spotify publiceerde op 3 september 2026 een artikel over hoe hun teams die kostenpost met ongeveer 90% hebben teruggebracht, en het mechanisme is ook zonder hun platform te reproduceren.

Wat Spotify precies heeft gemeten

Portal by Spotify is de commerciële distributie van Backstage. Het artikel, geschreven door Dimitri Mazmanov, beschrijft daarin modes: declaratieve agents die op een kortstondige runtime draaien, met een eigen model en eigen gekoppelde MCP-tools. Twee modes leveren het grootste deel van de winst op: bulk-reader, die een vraag beantwoordt door meerdere bestanden te lezen, en code-writer, die repetitieve code produceert op basis van bestaande patronen. De voorbeelden in het artikel laten ze draaien op Gemini 2.5 Flash.

Aan de kant van Claude Code is het de plugin shunt uit de publieke repository spotify/portal-ai-plugins (Apache-2.0) die de omweg afdwingt. De documentatie ervan beschrijft drie lagen: PreToolUse-hooks, met name check-file-size en check-bash-read, die lezingen boven de 350 regels blokkeren, een drempel die instelbaar is via de omgevingsvariabele SHUNT_MIN_LINES, bashscripts die de Portal-CLI aanroepen, en Markdown-skills die de agent uitleggen wanneer hij ze moet gebruiken. De aangekondigde besparing ligt rond de 90% op het massaal lezen van een Java-monorepo.

Het cijfer dat echt opvalt, staat elders in het artikel: een kwart van de ondervraagde engineering-managers zou al 200 tot 500 dollar per ontwikkelaar per maand aan tokens uitgeven, sommigen zelfs meer dan 2.000. Op Hacker News zet de discussie die op 4 september werd geopend (269 punten en 173 reacties toen ik hem op de 7e las) niet het principe ter discussie, maar wel de reikwijdte ervan: één reageerder meldt dat een goedkoop model een subtiele race condition over het hoofd zag, meerdere anderen wijzen erop dat de hoofdagent de bestanden vaak toch zelf herleest, waardoor de winst wegvalt. De regel die het vaakst terugkeert, past in één zin: het kleine model mag aanwijzen, niet beslissen.

Waarom kost het lezen van een bestand zo veel?

Versie 2.1.257 van Claude Code, uitgebracht op 1 september 2026, maakte Claude Fable 5.1 het standaardmodel: een context van een miljoen tokens, 10 $ per miljoen tokens input, 50 $ voor output, 0,25 $ per miljoen bij lezen uit de cache. Terug naar het bestand van het begin.

Bewerking op post.php Tokens Kosten, Fable 5.1-tarief
Volledige lezing via Read ≈ 87.000 input 0,87 $
Dezelfde inhoud herlezen vanuit de cache ≈ 87.000 cachelezing 0,02 $
Antwoord van een werkmodel (1.595 bytes) ≈ 456 input 0,005 $

Die 87.000 tokens betaal je niet één keer. Ze worden bij elke volgende beurt opnieuw meegestuurd. Zolang de cache stand houdt, kost dat 0,02 $ per beurt, zodra hij koud is, springt de rekening terug naar 0,87 $. De documentatie van Anthropic zegt het zonder omwegen: een vraag van één regel, gesteld in een sessie die al sinds de ochtend openstaat, trekt het verbruik van het hele gesprek mee. Ze geeft ook de ordes van grootte die in bedrijven zijn waargenomen, ongeveer 13 $ per ontwikkelaar per actieve dag, 150 tot 250 $ per maand, met 90% van de gebruikers onder de 30 $ per dag.

De hook PreToolUse schrijven

Een hook PreToolUse ontvangt op zijn standaardinvoer de JSON van de tool-aanroep, tool_name, tool_input, cwd, permission_mode, en geeft zijn beslissing terug op zijn standaarduitvoer. Drie uitkomsten: een exitcode 0 met {} laat de gewone permissieflow beslissen, een exitcode 0 met een object hookSpecificOutput geeft de doorslag, een exitcode 2 blokkeert hoe dan ook, met het bericht van stderr. Voor deze opzet is het de tweede vorm die nodig is: weigeren en uitleggen waar de agent dan heen moet.

De hook wordt gedeclareerd in het settings.json-bestand van het project, en is dus mee te versiebeheren met de repository:

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…"
          }
        ]
      }
    ]
  }
}

En hier het script, in de versie die ik heb uitgevoerd:

bash
#!/usr/bin/env bash
# bulk-read-guard.sh: PreToolUse-hook.
# Invoer: de JSON van de hook op stdin. Uitvoer: JSON op stdout, exitcode 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')
    # Een lezing die al onder de drempel begrensd is, kost bijna niets.
    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 en tail -20 blijven goedkoop.
    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

Drie details zijn belangrijk. De matcher dekt Read en Bash, anders omzeilt de agent de weigering met een cat. Een lezing die al met een limit onder de drempel begrensd is, komt zonder discussie door, anders stuurt de hook de agent in een lus terug naar de skill. En de permissionDecisionReason richt zich tot de agent, niet tot jou, het is de tekst die hij leest om te beslissen wat hij vervolgens doet, dus die moet het exacte pad van het te starten script noemen. Dit geeft de hook terug bij een weigering:

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é."
}

De skill achter de hook

De hook kan nee zeggen, werken kan hij niet. De skill neemt dat op zich, en alleen zijn metadata, naam en beschrijving, vullen de context zolang hij niet geactiveerd is, terwijl dezelfde instructies in een CLAUDE.md bij elke sessie worden geladen. Het formaat is dat van de standaard beschreven in onze gids over Agent Skills: een map, een SKILL.md, scripts ernaast.

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.

Het script dat door de skill wordt aangeroepen, past in een twaalftal regels. Het commando van het werkmodel loopt via een variabele, waardoor je die tijdens het testen kunt vervangen:

bash
#!/usr/bin/env bash
# resume-fichier.sh: stuurt een heel bestand naar een goedkoop model en geeft alleen het antwoord terug.
# Het bestand komt nooit in de context van de hoofdagent terecht.
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}"
# In productie: claude -p --model haiku. Tijdens het testen: 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

Wat de testopstelling opleverde

Een hook test je zonder agent: hij leest JSON op zijn standaardinvoer en geeft zijn beslissing terug op de uitvoer. Ik heb dus een testbank geschreven die de payloads bouwt die in de hooks-documentatie beschreven staan, ze naar het script stuurt en de teruggegeven beslissing vergelijkt met de verwachte. Tien gevallen, tien treffers:

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

Geval 09 is het leerzaamst: sed -n '1,4000p' komt erdoor. De hook kent alleen cat, head en tail, en elk ander leescommando glipt erdoorheen. Een hook verlaagt een gemiddelde uitgave, hij sluit geen deur.

Rest het modeldeel. Ik heb de variabele SHUNT_CMD gekoppeld aan codex exec op GPT-6 Astra, uit de vergelijking tussen Astra en Fable 5.1, geen goedkoop model, maar wel een agent waarmee je de hele leiding van begin tot eind kunt meten:

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          # in rekening gebracht bij het werkmodel
$ wc -lc < resultats/resume-codex.txt
      10    1595            # wat teruggaat naar de hoofdagent

Zesendertig seconden, 91.805 tokens in rekening gebracht bij het werkmodel, en 1.595 bytes die teruggaan naar de hoofdagent: een verhouding van 191 op 1 in wat de context binnenkrijgt. Het antwoord is met de hand gecontroleerd, met grep: zes aanroepen van current_user_can(), op de regels 3436, 3489, 4734, 4736, 5105 en 7705, alle zes de nummers kloppen. Met een deterministisch alternatief, een gewone grep op de declaraties in plaats van het model, levert dezelfde opzet 3.460 bytes op in 0,56 seconde: als de vraag structureel is, hoeft de werker niet altijd een model te zijn.

Wat kost Claude Code nu eigenlijk?

Bij de API is alles terug te vinden in de tarieventabel hierboven. Bij een abonnement draait de vraag om de limieten. Op 31 augustus 2026 liep de promotie “+50%” op de wekelijkse limieten af, vervangen door een permanente verhoging van 25% van de basislimiet, wat voor wie van de promotie profiteerde neerkomt op ongeveer 17% minder dan voorheen, iets wat diezelfde dag nog een discussie op Hacker News vulde. In beide gevallen is het commando /usage het vertrekpunt: het toont de kosten van de sessie, de verdeling per model, en sinds versie 2.1.251 een regel “Prompt cache” die het aandeel inputtokens uit de cache en het aantal missers weergeeft. Bij een abonnement voegt het ook het verbruiksaandeel toe dat wordt toegeschreven aan skills, subagents, plugins en elke MCP-server.

De hendels om om te zetten voordat je iets installeert

  • De promptcache. Die duurt een uur bij een abonnement, vijf minuten bij verbruikskrediet of een API-sleutel. Meldt de regel “Prompt cache” van /usage missers, zoek dan de oorzaak vóór elke andere optimalisatie: een tooldefinitie die tijdens de sessie verandert, is al genoeg om alles te herschrijven.
  • Subagents. De uitvoerige output van een testsuite of een logbestand blijft in hun context, alleen de samenvatting komt terug. Voor eenvoudige taken raadt de documentatie model: haiku aan in de configuratie van de subagent: dat is de ingebouwde versie van het idee van Spotify, zonder platform om te installeren.
  • Opruimen in de skills. Een geladen maar nooit aangeroepen skill betaalt toch zijn metadata bij elke sessie. Het commando /skill-doctor, dat met versie 2.1.261 van 4 september kwam, somt precies die skills op met hun kosten in context, we wijden er een volledig artikel aan.

Spotify heeft op dit punt trouwens niets uitgevonden. De documentatie over kosten geeft een eigen voorbeeld van een filterhook, een PreToolUse die het testcommando herschrijft zodat alleen de mislukkingen worden teruggegeven, met het veld updatedInput in plaats van een weigering. Herschrijven in plaats van weigeren is vaak zachter, de agent verliest zijn beurt niet.

Wat je moet onthouden

  • De opzet past in twee bestanden: een hook PreToolUse die boven een regeldrempel weigert, een skill die het werk elders laat doen.
  • De weigering moet via permissionDecision: "deny" lopen en een permissionDecisionReason die het alternatieve script noemt, niet via een exitcode 2.
  • Op een bestand van 8.861 regels krijgt de hoofdagent 1.595 bytes te verwerken in plaats van 305.215, oftewel 191 op 1.
  • Dek Bash net zo goed als Read, en laat lezingen door die al met een limit begrensd zijn.
  • Voordat je iets installeert: /usage, de promptcache, subagents met een goedkoop model.

Veelgemaakte fouten

Read blokkeren en Bash vergeten De agent omzeilt de weigering met cat. De matcher moet Read|Bash dekken en het script moet .tool_input.command controleren.
Een al begrensde lezing weigeren Een Read met een limit onder de drempel kost bijna niets. Zonder die test stuurt de hook de agent in een lus terug naar de skill, en betaal je de omweg voor niets.
Afsluiten met code 2 om te blokkeren Code 2 blokkeert hoe dan ook, en het bericht komt van stderr. Om de agent te vertellen wat hij in plaats daarvan moet doen, sluit je af met code 0, permissionDecision: deny en een permissionDecisionReason die het script noemt.
Denken dat de hook de deur sluit sed -n '1,4000p' komt erdoor, net als elk niet-voorzien leescommando. Een hook verlaagt een gemiddelde, hij garandeert niets.
Een gevoelig bestand laten samenvatten Het script stuurt het hele bestand naar een ander model, vaak bij een andere provider. Sluit .env-bestanden, dumps en alles met inloggegevens uit.

Claude CodeMCPPerformanceSkills

Damien Flandrin Webdeveloper sinds 2010, maker van Gekkode en Email Impact. Elk artikel wordt vóór publicatie getest op een echt project. Contact
Nieuwsbrief

Nieuwe tests, tutorials en projecten, per e-mail.

Reproduceerbare tests, geversioneerde code, gedateerde resultaten. Nooit spam.