Preço do Claude Code: reduzir a fatura com hooks e skills

A Spotify anuncia menos 90% de tokens de leitura graças a um hook PreToolUse e a skills. Reproduzi a montagem sem a plataforma deles, num ficheiro PHP de 8 861 linhas.

Preço do Claude Code: reduzir a fatura com hooks e skills
Resposta rápida

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:

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

E aqui está o script, na versão que executei:

bash
#!/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 0

Trê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:

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

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.

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.

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:

bash
#!/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_CMD

O 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
$ 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

O 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:

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          # faturados ao modelo operário
$ wc -lc < resultats/resume-codex.txt
      10    1595            # o que volta ao agente principal

Trinta 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 /usage anunciar 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: haiku na 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 PreToolUse que recusa acima de um limiar de linhas, um skill que manda fazer o trabalho noutro lado.
  • A recusa deve passar por permissionDecision: "deny" e um permissionDecisionReason que 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 Bash tanto quanto o Read, e deixa passar as leituras já limitadas por um limit.
  • Antes de instalar seja o que for: /usage, a cache de prompt, os subagentes em modelo barato.

Erros frequentes

Bloquear 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.
Recusar uma leitura já limitada Um 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.
Sair em código 2 para bloquear O código 2 bloqueia sempre e a mensagem vem do stderr. Para dizer ao agente o que fazer em vez disso, sai em código 0 com permissionDecision: deny e um permissionDecisionReason que indique o script.
Achar que o hook fecha a porta sed -n '1,4000p' passa, como qualquer comando de leitura não previsto. Um hook faz baixar uma média, não garante nada.
Mandar resumir um ficheiro sensível O script envia o ficheiro inteiro a outro modelo, muitas vezes noutro fornecedor. Exclui os .env, os dumps e tudo o que tenha credenciais.

Claude CodeMCPPerformanceSkills

Damien Flandrin Programador web desde 2010, criador da Gekkode e do Email Impact. Cada artigo é testado num projeto real antes de ser publicado. Contacto
Newsletter

Os novos testes, tutoriais e projetos, por e-mail.

Testes reproduzíveis, código versionado, resultados datados. Nunca spam.