
Um hook PreToolUse recusa as leituras de ficheiros acima de um limiar de linhas e reencaminha o agente para um skill que manda resumir o ficheiro por um modelo mais barato. Num ficheiro de 8 861 linhas e 305 215 bytes, o contexto do agente principal encaixa 1 595 bytes em vez de 305 215, ou seja, um rácio de 191 para 1. Na Spotify, a mesma montagem faz cair cerca de 90% os tokens de leitura de um monorepo Java.
Uma leitura de ficheiro não parece nada de especial até a contares: o wp-includes/post.php de uma instalação WordPress pesa 8 861 linhas e 305 215 bytes, ou seja, cerca de 87 000 tokens que entram no contexto e aí ficam até ao próximo /clear. A Spotify publicou a 3 de setembro de 2026 um artigo a explicar como as suas equipas fizeram cair esta despesa em cerca de 90%, e o mecanismo reproduz-se sem a plataforma deles.
O que a Spotify mediu realmente
O Portal by Spotify é a distribuição comercial do Backstage. O artigo, assinado por Dimitri Mazmanov, descreve aí modes: agentes declarativos que correm num runtime efémero, com o seu próprio modelo e as suas próprias ferramentas MCP associadas. Dois modes carregam a maior parte do ganho: bulk-reader, que responde a uma pergunta lendo vários ficheiros, e code-writer, que produz código repetitivo a partir de padrões existentes. Os exemplos do artigo fazem-nos correr sobre o Gemini 2.5 Flash.
Do lado do Claude Code, é o plugin shunt do repositório público spotify/portal-ai-plugins (Apache-2.0) que impõe o desvio. O seu ficheiro de documentação descreve três camadas: hooks PreToolUse, check-file-size e check-bash-read, que bloqueiam as leituras acima de 350 linhas, limiar ajustável pela variável de ambiente SHUNT_MIN_LINES, scripts bash que chamam a CLI Portal, skills em Markdown que explicam ao agente quando os usar. A poupança anunciada ronda os 90% na leitura em massa de um monorepo Java.
O número que incomoda está noutro ponto do artigo: um quarto dos responsáveis de engenharia inquiridos já gastaria entre 200 e 500 dólares por programador e por mês em tokens, alguns acima de 2 000. No Hacker News, o tópico aberto a 4 de setembro (269 pontos e 173 comentários quando o li, a 7) não contesta o princípio mas sim o seu alcance: um comentador relata que um modelo barato deixou passar uma falha de concorrência subtil, vários lembram que o agente principal acaba muitas vezes por reler os ficheiros ele próprio, o que anula o ganho. A regra que mais se repete cabe numa frase: o modelo pequeno tem o direito de apontar, não de decidir.
Porque é que uma leitura de ficheiro custa tão caro?
A versão 2.1.257 do Claude Code, publicada a 1 de setembro de 2026, tornou o Claude Fable 5.1 no modelo predefinido: contexto de um milhão de tokens, 10 $ por milhão de tokens em entrada, 50 $ em saída, 0,25 $ o milhão em leitura de cache. Retomemos o ficheiro do início.
| Operação sobre post.php | Tokens | Custo, tarifa Fable 5.1 |
|---|---|---|
Leitura integral por Read | ≈ 87 000 em entrada | 0,87 $ |
| O mesmo conteúdo relido a partir da cache | ≈ 87 000 em leitura de cache | 0,02 $ |
| Resposta de um modelo operário (1 595 bytes) | ≈ 456 em entrada | 0,005 $ |
Estes 87 000 tokens não se pagam uma vez só. São reenviados em cada turno seguinte. Enquanto a cache aguenta, voltam a custar 0,02 $ por turno, assim que arrefece, a conta volta a 0,87 $. A documentação da Anthropic di-lo sem rodeios: uma pergunta de uma linha feita numa sessão aberta desde a manhã puxa o custo de uso para toda a conversa. Dá também as ordens de grandeza observadas em empresas, cerca de 13 $ por programador e por dia ativo, 150 a 250 $ por mês, com 90% dos utilizadores abaixo de 30 $ por dia.
Escrever o hook PreToolUse
Um hook PreToolUse recebe na sua entrada padrão o JSON da chamada da ferramenta, tool_name, tool_input, cwd, permission_mode, e devolve a sua decisão na saída padrão. Três desfechos: um código de saída 0 com {} deixa o fluxo de permissões habitual decidir, um código 0 com um objeto hookSpecificOutput decide, um código 2 bloqueia sempre, com a mensagem tirada do stderr. Para esta montagem, é a segunda forma que é precisa: recusar e explicar para onde ir.
O hook declara-se no settings.json do projeto, portanto versionável com o repositório:
{
"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…"
}
]
}
]
}
}E aqui está o script, na versão que executei:
#!/usr/bin/env bash
# bulk-read-guard.sh: hook PreToolUse.
# Entrada: o JSON do hook em stdin. Saída: JSON em stdout, código de saída 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')
# Uma leitura já limitada abaixo do limiar não custa quase nada.
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 e tail -20 continuam baratos.
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 0Três detalhes contam. O matcher cobre Read e Bash, sem o que o agente contorna a recusa com um cat. Uma leitura já limitada por um limit abaixo do limiar passa sem discussão, senão o hook devolve o agente ao skill em ciclo. E o permissionDecisionReason dirige-se ao agente, não a ti, é o texto que ele lê para decidir o que fazer a seguir, por isso tem de indicar o caminho exato do script a executar. Eis o que o hook devolve numa recusa:
{
"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é."
}O skill por trás do hook
O hook sabe dizer que não, não sabe trabalhar. É o skill que trata disso, e só os seus metadados, nome e descrição, ocupam o contexto enquanto não é acionado, ao passo que as mesmas instruções colocadas num CLAUDE.md são carregadas em cada sessão. O formato é o do standard descrito no nosso guia dos Agent Skills: uma pasta, um SKILL.md, scripts ao lado.
---
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.O script chamado pelo skill cabe numa dúzia de linhas. O comando do modelo operário passa por uma variável, o que permite substituí-lo durante os testes:
#!/usr/bin/env bash
# resume-fichier.sh: envia um ficheiro inteiro a um modelo barato e só devolve a resposta.
# O ficheiro nunca atravessa o contexto do agente 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}"
# Em produção: claude -p --model haiku. Durante o teste: 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_CMDO que deu o banco de testes
Um hook testa-se sem agente: lê JSON na sua entrada padrão e devolve a sua decisão na saída. Escrevi então um banco de testes que fabrica as cargas úteis descritas na documentação dos hooks, envia-as ao script e compara a decisão devolvida com a esperada. Dez casos, dez conformidades:
$ 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=0O caso 09 é o mais instrutivo: sed -n '1,4000p' passa. O hook só conhece cat, head e tail, e qualquer outro comando de leitura atravessa-o. Um hook reduz uma despesa média, não fecha uma porta.
Falta a parte do modelo. Liguei a variável SHUNT_CMD ao codex exec sobre o GPT-6 Astra, o do comparativo entre o Astra e o Fable 5.1, que não é um modelo barato, mas que mede a canalização de ponta a ponta:
$ 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 # faturados ao modelo operário
$ wc -lc < resultats/resume-codex.txt
10 1595 # o que volta ao agente principalTrinta e seis segundos, 91 805 tokens faturados ao modelo operário, e 1 595 bytes que voltam ao agente principal: um rácio de 191 para 1 sobre o que o contexto encaixa. A resposta foi verificada à mão, com grep: seis chamadas a current_user_can(), linhas 3436, 3489, 4734, 4736, 5105 e 7705, os seis números estão certos. Com um substituto determinístico, um simples grep das declarações em vez do modelo, a mesma montagem devolve 3 460 bytes em 0,56 segundos: quando a pergunta é estrutural, o operário nem sempre precisa de ser um modelo.
Quanto custa o Claude Code, ao certo?
Na API, tudo se lê na grelha recordada acima. Na assinatura, a questão passa a ser a dos limites. A 31 de agosto de 2026, a promoção «+50%» sobre os limites semanais terminou, substituída por um aumento permanente de 25% do limite de base, ou seja, para quem aproveitava a promoção, cerca de 17% a menos do que antes, o que ocupou um tópico do Hacker News nesse mesmo dia. Nos dois casos, o comando /usage é o ponto de partida: mostra o custo da sessão, a repartição por modelo, e desde a versão 2.1.251 uma linha «Prompt cache» que dá a parte dos tokens de entrada servidos pela cache e o número de falhas. Numa assinatura, acrescenta a parte do consumo atribuída aos skills, aos subagentes, aos plugins e a cada servidor MCP.
As alavancas a ativar antes de instalar seja o que for
- A cache de prompt. Dura uma hora numa assinatura, cinco minutos com créditos de uso ou uma chave de API. Se a linha «Prompt cache» de
/usageanunciar falhas, procura a causa antes de qualquer outra otimização: uma definição de ferramenta que muda a meio da sessão basta para reescrever tudo. - Os subagentes. A saída verbosa de uma suite de testes ou de um registo fica no contexto deles, só o resumo sobe. Para as tarefas simples, a documentação recomenda
model: haikuna configuração do subagente: é a versão integrada da ideia da Spotify, sem plataforma nenhuma para instalar. - A limpeza dos skills. Um skill carregado mas nunca invocado paga na mesma os seus metadados em cada sessão. O comando
/skill-doctor, chegado com a versão 2.1.261 de 4 de setembro, lista precisamente esses e o seu custo em contexto, dedicamos-lhe um artigo inteiro.
A Spotify, aliás, não inventou nada neste ponto. A documentação dos custos dá o seu próprio exemplo de hook de filtragem, um PreToolUse que reescreve o comando de teste para só mostrar as falhas, usando o campo updatedInput em vez de uma recusa. Reescrever em vez de recusar é muitas vezes mais suave, o agente não perde a vez.
A reter
- A montagem cabe em dois ficheiros: um hook
PreToolUseque recusa acima de um limiar de linhas, um skill que manda fazer o trabalho noutro lado. - A recusa deve passar por
permissionDecision: "deny"e umpermissionDecisionReasonque indique o script alternativo, não por um código de saída 2. - Num ficheiro de 8 861 linhas, o agente principal encaixa 1 595 bytes em vez de 305 215, ou seja, 191 para 1.
- Cobre o
Bashtanto quanto oRead, e deixa passar as leituras já limitadas por umlimit. - Antes de instalar seja o que for:
/usage, a cache de prompt, os subagentes em modelo barato.
Erros frequentes
Read e esquecer o Bash O agente contorna a recusa com cat. O matcher deve cobrir Read|Bash e o script inspecionar .tool_input.command.Read com um limit abaixo do limiar não custa quase nada. Sem este teste, o hook devolve o agente ao skill em ciclo e pagas o desvio para nada.permissionDecision: deny e um permissionDecisionReason que indique o script.sed -n '1,4000p' passa, como qualquer comando de leitura não previsto. Um hook faz baixar uma média, não garante nada..env, os dumps e tudo o que tenha credenciais.

