
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:
{
"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:
#!/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 0Drie 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:
{
"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.
---
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:
#!/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_CMDWat 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 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=0Geval 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:
$ 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 hoofdagentZesendertig 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
/usagemissers, 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: haikuaan 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
PreToolUsedie boven een regeldrempel weigert, een skill die het werk elders laat doen. - De weigering moet via
permissionDecision: "deny"lopen en eenpermissionDecisionReasondie 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
Bashnet zo goed alsRead, en laat lezingen door die al met eenlimitbegrensd 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.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.permissionDecision: deny en een permissionDecisionReason die het script noemt.sed -n '1,4000p' komt erdoor, net als elk niet-voorzien leescommando. Een hook verlaagt een gemiddelde, hij garandeert niets..env-bestanden, dumps en alles met inloggegevens uit.

