Pular para o conteúdo principal

Plugin Evals: teste seu plugin com claude plugin eval

Avançado
What you'll learn
  • Entender o que um plugin eval mede e que o `claude plugin validate` e um teste manual não medem: a skill *dispara* e supera um modelo sem plugin?
  • Ler os três números de cada resultado — WITH, W/OUT e Δ — e saber por que alguns graders são deliberadamente excluídos da pontuação
  • Escolher entre os seis tipos de grader (quatro gratuitos, dois baseados em juiz) e escrever rubricas que deem um sinal estável
  • Simular os servidores MCP de um plugin para que uma suíte seja repetível e nunca toque o serviço real
  • Montar um portão de CI com modelos fixados, um teto de custo e o contrato de códigos de saída

Até agora, quem escrevia um plugin tinha duas ferramentas: claude plugin validate, que confere se manifestos e frontmatter são lidos, e os próprios olhos. Nenhuma responde à pergunta que decide se vale a pena instalar um plugin: quando o usuário digita um pedido natural, o Claude escolhe a skill, e o resultado é melhor do que o Claude faria de qualquer jeito? O claude plugin eval responde exatamente isso. Ele roda cada caso numa sessão descartável só com o seu plugin carregado, roda de novo sem nenhum plugin, pontua os dois e reporta a diferença.

Esta página é para quem já tem um plugin ou uma skill funcionando. Se você só precisa do conceito, leia as duas primeiras seções e o quiz.

O modelo mental: dois braços e um delta​

Todo caso é um prompt mais um ou mais graders. Um grader é uma checagem de passou/não passou sobre o que o Claude produziu. Para cada caso, o Claude Code executa:

  • o braço com plugin (with-arm): uma sessão headless nova só com o seu plugin carregado, o prompt enviado, o Claude trabalhando até terminar ou atingir o limite de turnos/tempo, e então os graders aplicados;
  • o braço sem plugin (without-arm): o mesmo prompt, o mesmo número de execuções, sem plugin.

Cada braço roda o caso três vezes por padrão, porque uma única execução de um agente não determinístico quase não diz nada. A pontuação de uma execução é a fração de graders que passaram (ponderada, se você definir pesos). A pontuação do caso é a média das execuções. Você obtém:

ColunaSignificado
WITHPontuação média com o plugin carregado
W/OUTPontuação média sem plugin
ΔWITH − W/OUT — o que o plugin realmente contribuiu

A parte nada óbvia: uma pontuação WITH alta, sozinha, não prova nada. Se um caso marca 1.0 nos dois braços, o Claude resolveu sem você e o seu plugin é peso morto para esse prompt. O Δ é o número que justifica a existência de um plugin. A própria documentação da Anthropic cita a primeira descoberta mais comum: um Δ perto de zero com um grader skill-fired falhando, o que significa que o Claude nunca escolheu a sua skill com uma formulação natural. Isso é um problema de description no SKILL.md, não de código, e nenhum teste manual em que você invoca a skill pelo nome teria pegado.

Por que alguns graders ficam fora da pontuação​

Uma checagem como "a ferramenta Skill foi invocada" nunca pode passar no braço sem plugin. Se contasse, arrastaria o W/OUT para zero e inflaria o Δ de graça. Por isso, numa execução de dois braços, o Claude Code exclui da pontuação, nos dois braços, todo grader tool_used cujo tool seja Skill, além de tudo o que você marcar com arm: with-only. Eles ainda aparecem no relatório como indicadores de passou/não passou (o relatório os rotula como "plugin-fired indicator") e o JSON os marca scored: false. Daí saem duas regras de borda:

  • Se todos os graders de um caso fossem excluídos, eles são pontuados normalmente; do contrário não sobraria nada para pontuar.
  • arm: both força um grader a contar nos dois braços. É o que você quer para uma checagem de "não deve invocar a skill" num prompt isca, escrita como tool_used com min: 0 e max: 0.

Consequência que vale lembrar: a mesma suíte pode relatar uma pontuação absoluta diferente com --ablation none (um braço só, nada excluído) do que no modo padrão de dois braços. Compare coisas comparáveis ao traçar tendências.

Anatomia de um caso​

Uma suíte vive em evals/ dentro do plugin (ou em outro diretório que você indicar). Cada caso é uma pasta com um prompt.md, um case.yaml ou ambos, mais uma pasta graders/ com um arquivo Markdown por grader.

my-plugin/
├── .claude-plugin/plugin.json
├── skills/...
└── evals/
├── drafts-commit-message/
│ ├── prompt.md # frontmatter: run limits; body: the user's request
│ └── graders/
│ ├── criteria.md # type: llm — rubric in the body
│ └── skill-fired.md # type: tool_used, tool: Skill
├── ignores-unrelated-request/
│ └── ...
├── mocks/ # optional suite-wide MCP mocks
└── results/ # written by each run — add to .gitignore

O corpo do prompt é enviado ao Claude exatamente como escrito. Duas coisas pegam quem está começando:

  • Cada execução começa num diretório de trabalho vazio, com home descartável, sem configurações de usuário, sem CLAUDE.md, sem servidores MCP pessoais, sem outros plugins e só com uma lista de variáveis de ambiente permitidas (as básicas como PATH, a autenticação do provedor, a maioria de ANTHROPIC_*/CLAUDE_CODE_* e tudo que se chame EVAL_*). Se a tarefa precisa de arquivos, coloque-os no prompt, gere-os com um scaffold ou inclua-os no plugin.
  • Menções @path no prompt não são expandidas em anexos. Conceda Read em allowed_tools se o Claude precisar abrir um arquivo.

Os campos do frontmatter de prompt.md que mais importam:

CampoPadrãoObservação
runs3De 1 a 50 por braço; --runs sobrescreve
max_turns10Até 200. Atingi-lo é registrado como erro de execução e normalmente baixa a pontuação, então seja generoso
timeout_seconds300Até 3600 por execução
allowed_tools[]Ferramentas somente leitura são concedidas só de listá-las; todo o resto precisa de uma concessão pela CLI
modelpadrão da sessão--model sobrescreve; fixe-o no CI
env{}As chaves devem casar com EVAL_[A-Z0-9_]* ou a execução falha
pluginso plugin envolvente mais próximoDefina ["../.."] se a detecção automática não achar o seu plugin

O case.yaml carrega os mesmos campos (os de execução sob execution:) mais três que referenciam outros arquivos: context.scaffold_script (um script Bash que prepara o espaço de trabalho, só roda com --scaffold), context.history_file (uma transcrição .jsonl a retomar, de modo que o seu prompt vira o próximo turno) e context.add_dirs (diretórios de fixtures que o Claude pode ler).

Os seis tipos de grader​

Quatro são calculados a partir da transcrição e dos arquivos e não custam nada. Dois chamam um modelo juiz e aumentam a conta.

TipoGratuito?Passa quando
regexsimUma regex JavaScript é encontrada no alvo (last_message por padrão, ou trace, files, um arquivo específico ou mock_calls). match: not_contains para ausência, match: "count:N" para uma contagem exata. A insensibilidade a maiúsculas vai em flags: i; o (?i) inline não é suportado
tool_usedsimAs chamadas a tool cuja entrada codificada em JSON casa com input_match ficam entre min (padrão 1) e max (ilimitado). min: 0, max: 0 afirma que uma ferramenta nunca foi chamada
tool_ordersimAmbas as ferramentas foram chamadas e a primeira ocorrência de before precede a primeira de after
file_existssimUm arquivo criado pelo Claude casa com o glob de path (ou nenhum casa, com exists: false). Arquivos criados por um scaffold ou apenas editados pelo Claude não contam
llmnãoUm modelo juiz vota PASS na sua rubrica em pelo menos dois de três votos
baselinenãoUm juiz considera que a execução cumpre os critérios pelo menos tão bem quanto uma transcrição de referência que você salvou como baseline_file

Não existem graders de código customizado. Se você precisa checar que um build ou teste passou, faça o prompt mandar o Claude rodá-lo e gravar o resultado num arquivo, pontue esse arquivo com regex e afirme que o comando rodou com um grader tool_used cujo input_match o nomeie.

O grader skill-fired que quase todo caso quer fica assim (troque o nome da skill; o padrão também casa a forma com namespace plugin:skill):

graders/skill-fired.md — did my skill actually run?

---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:[\w-]+:)?your-skill-name"'
---

E a rubrica do juiz ao lado, escrita como condições concretas de PASS/FAIL em vez de adjetivos:

graders/criteria.md — a rubric that gives a stable verdict

---
type: llm
weight: 2
---

PASS if the reply is a single conventional-commit subject line under 72 characters,
starts with "refactor:", and mentions both the rename (getUser -> fetchUser) and the
number of call sites updated.
FAIL if the reply contains more than one candidate message, asks a clarifying
question, or omits the rename.

Hábitos que mantêm as pontuações estáveis​

São as práticas que a documentação oficial recomenda e batem com o que todo mundo que já rodou pipelines de LLM-as-judge aprende na marra:

  • Pontue saídas longas com regex sobre o arquivo, não com um juiz llm. A variância do juiz cresce com o tamanho do que ele lê. Reserve os graders llm para mensagens finais curtas.
  • Um grader no resultado, outro no caminho. Um grader regex/llm/file_exists diz que a resposta estava certa; um grader tool_used/tool_order diz que o seu plugin a produziu. Você precisa dos dois para interpretar o Δ.
  • Desconfie do juiz antes do plugin quando o Δ é negativo mas a skill disparou. O juiz padrão é um modelo pequeno e rápido; ele pode reprovar uma resposta correta por estar formatada diferente da rubrica. Rode de novo com --judge-model sonnet e aperte a rubrica para que a formatação não decida.
  • Itere num caso com um braço, depois confirme com três execuções: --case <name> --runs 1 --ablation none. Com um braço só, a tabela mostra SCORE e PASS% em vez de WITH/W/OUT/Δ.

Simule os servidores MCP​

Se as skills do seu plugin chamam ferramentas MCP, uma execução nunca inicia os servidores reais a menos que você peça. Em vez disso, o Claude Code registra um substituto sob o nome de cada servidor e responde às chamadas de ferramenta a partir de arquivos Markdown: evals/mocks/<server>/<tool>.md para a suíte inteira, ou a pasta mocks/ do próprio caso para sobrescrever por caso. O corpo do arquivo é o resultado da ferramenta, com substituições {{input.<field>}} e inserções {{file:fixtures/...}}. Uma ferramenta sem arquivo de mock simplesmente não está disponível para o Claude.

evals/mocks/tracker/create_issue.md — a mock that also asserts the input

---
expect:
title: string
priority: [low, medium, high]
---

Created issue #4821: {{input.title}}

O bloco expect: transforma um mock numa asserção: uma chamada que o viole aborta a execução com pontuação 0 e registra o servidor, a ferramenta e o motivo. Aponte um grader para target: mock_calls para pontuar o que o plugin pediu ao servidor. Para servidores cujas respostas dependem da conversa, type: agent deixa um modelo pequeno fazer o papel do servidor; execuções limpas salvam as respostas dele em results/<timestamp>/mock-recordings/, e quando você copia uma gravação para mocks/.replay/<server>/, as execuções seguintes a reproduzem sem chamar o modelo. Faça commit de .replay/ para o CI ser determinístico. _tools.json (uma resposta tools/list salva) dá às ferramentas simuladas suas descrições e esquemas reais em vez de um marcador permissivo.

Para usar servidores reais: --allow-real-servers inicia os que não têm mocks; --mocks off ignora os mocks por completo. De qualquer forma, esses processos rodam como você, fora do sandbox, e suas ferramentas ainda precisam de uma concessão explícita como --allow-tools "mcp__plugin_my-plugin_github__*".

Executando​

Guided walkthrough1 of 5
  1. A partir da raiz do plugin, rode `claude plugin eval init`. Ele abre uma sessão interativa em que o Claude lê o plugin, pergunta como é um bom resultado, propõe prompts que devem e que não devem dispará-lo, projeta graders, testa uma vez e escreve um diretório de caso por prompt. `claude plugin eval init --bare <name>` escreve um modelo em branco (a única forma que funciona sem terminal, por exemplo no CI).

O comando para a suíte completa com os flags que você realmente vai usar:

Run the suite with a stronger judge and a cost ceiling

claude plugin eval . \
--judge-model sonnet \
--max-cost-usd 10 \
--allow-tools Read Write "Bash(npm test *)" \
-j 4

Notas sobre os flags: -j/--concurrency vai de 1 a 8 e só encurta o tempo de relógio, porque todas as execuções dividem o limite de requisições da sua conta. --max-cost-usd é um teto sobre a estimativa a preço de tabela, conferido antes de cada execução começar; as execuções já em andamento terminam, então o gasto pode ultrapassar o teto por essas execuções, e qualquer coisa não iniciada faz o comando sair com 2 e partial: true. Coloque o alvo (.) antes de --tag, --allow-tools e --json: cada um deles recebe uma lista ou um valor opcional e engoliria um alvo que viesse depois (o erro "--json output path must end in .json" é exatamente esse deslize).

Quanto custa​

Cada execução e cada voto do juiz é uma chamada real a um modelo na sua conta, contando contra o uso do seu plano ou a sua fatura de API. A aritmética aproximada: cases × runs execuções de agente para o braço com plugin, o mesmo de novo para o braço sem plugin, mais três chamadas curtas ao juiz por grader llm ou baseline por execução. O caso único do passo a passo oficial, com dois graders (um llm, um tool_used), rodou seis execuções de agente em 74 segundos a uma estimativa de preço de tabela de cerca de $0.41. Três alavancas mantêm uma suíte acessível:

  • --ablation none corta pela metade as execuções de agente quando você está iterando graders e não precisa do Δ.
  • Só graders gratuitos (regex, tool_used, tool_order, file_exists) para a suíte de cada commit; graders com juiz para a noturna.
  • Um erro de limite de uso ou de taxa no meio da suíte faz cada execução seguinte terminar com esse erro e normalmente pontuar 0, e a suíte não é marcada como partial, então uma execução limitada pode parecer exatamente uma regressão. Confira a coluna NOTES ou cases[].arms.with[].error antes de acreditar numa queda repentina.

Bloqueie o CI com isso​

CI job — pinned models, local report, exit code drives the build

claude plugin eval . \
--trust-plugin \
--json results.json \
--threshold 0.8 \
--model claude-sonnet-5 \
--judge-model claude-haiku-4-5 \
--no-publish \
--max-cost-usd 20

Por que cada flag está ali:

  • --trust-plugin afirma a decisão de confiança da primeira execução. Sem ele, um job não interativo é recusado com saída 1 (ou trava no aviso se o runner alocar um TTY). Passe-o só para plugins cujo código e suíte você rodaria na sua própria máquina.
  • --model e --judge-model são fixados para que a implantação de um modelo nunca seja confundida com uma regressão do plugin. Isso importa mais do que parece: o agente sob teste usa por padrão ANTHROPIC_MODEL ou o modelo padrão atual do Claude Code, que muda com os lançamentos.
  • --threshold 0.8 porque o padrão é 1.0, o que faz o comando sair com 1 sempre que algum caso for menos que perfeito. Uma barra de 1.0 sobre a média de três execuções de um agente não determinístico é um portão instável.
  • --json results.json escreve o documento de resultado versionado (schemaVersion: 1, camelCase, campos novos são adicionados sem renomear) e silencia a saída de progresso.

O contrato de códigos de saída:

SaídaSignificado
0Todos os casos no limiar ou acima e todos os arquivos de caso carregados
1Um caso abaixo do limiar, um arquivo de caso falhou ao carregar, nenhum caso encontrado, uma execução não pôde começar, diretório não confiável sem --trust-plugin, ou uma opção inválida
2Execução parcial: teto de custo atingido, ou credencial rejeitada na primeira execução ou antes. O results.json ainda é escrito com partial: true
130 / 143Interrompida / terminada (por exemplo, timeout do CI). Resultados parciais são escritos

Problemas ao escrever ou publicar o relatório nunca mudam o código de saída. Ao traçar tendências, descarte documentos com partial: true e execuções marcadas com skippedPaidGraders, já que suas pontuações não são comparáveis.

Campos que um script de portão lê do JSON: aggregates.overallScore, aggregates.casesPassed / casesTotal, aggregates.meanDelta e, por caso, cases[].aggregates.score e .delta (omitido quando os braços não são comparáveis). Uma execução com cases[].arms.with[].error não nulo (por exemplo, timed out after 300s) ainda é pontuada pelo que produziu, então um erro não implica pontuação 0; uma execução aborted (o expect: de um mock disparou) pontua 0 com error ainda null.

O que uma execução pode e não pode alcançar​

Leia isto antes de avaliar um plugin que você não escreveu. Apontar o claude plugin eval para um diretório é a mesma decisão de confiança que claude --plugin-dir: as skills e os hooks do plugin são carregados e rodam na sua máquina como você. O isolamento limita o que o agente sob teste pode fazer; não é uma fronteira contra o código do próprio plugin, e uma suíte que passa não diz nada sobre o plugin ser seguro.

  • Agente isolado. Cada execução recebe um home, um diretório de trabalho e uma configuração do Claude Code descartáveis, roda como um processo filho claude -p, não consegue ler o diretório de evals (então nunca vê os prompts nem os graders do caso ou dos casos vizinhos) e tem a ferramenta Artifact desligada.
  • Configurações gerenciadas continuam valendo. Restrições implantadas por um administrador na máquina valem dentro de uma execução, então resultados num notebook gerenciado podem diferir dos de um runner de CI não gerenciado.
  • Válvulas de escape opt-in. O scaffold_script de um caso roda como você, fora do sandbox, e só com --scaffold. Servidores MCP reais só com --allow-real-servers ou --mocks off. O allowed_tools de um caso ou o frontmatter allowed-tools de uma skill nunca podem ampliá-los.
  • Rede. Comandos de shell que você concede seguem as regras de rede do sandbox; uma concessão WebFetch(domain:…) alcança esse domínio diretamente; os hooks do plugin e qualquer servidor MCP real que você inicie podem alcançar qualquer host.

Para um plugin de terceiros que traz hooks ou precisa dos seus servidores reais, trate as pontuações como orientativas, a menos que você tenha rodado a suíte num contêiner ou num runner de CI. Veja Reviewing Third-Party Code para a lista de verificação mais ampla.

Referência rápida: as armadilhas​

  • O target padrão de regex é last_message. Quando você aponta para trace, é JSON por linha, então as aspas aparecem como \" e as quebras de linha são escapadas.
  • files é uma lista de caminhos que o Claude criou, não o conteúdo deles. Para pontuar o conteúdo, use { source: file, path: <path> } como target/focus. Um juiz llm vê PNG/JPEG/GIF/WebP como imagens e recusa outros binários (.pptx, PDF).
  • Um juiz llm que lê trace vê apenas as primeiras 12 e as últimas 12 mensagens.
  • file_exists só vê arquivos criados durante a execução. Um arquivo gerado por scaffold ou apenas editado é invisível para ele; pontue o conteúdo ou use tool_used em Edit.
  • Chaves desconhecidas no frontmatter de prompt.md são um erro, e as chaves de env devem ser EVAL_*.
  • Avaliar um plugin instalado pelo nome (name@marketplace) escreve os resultados em ./evals/results/ no seu diretório atual e pula o aviso de confiança.
  • O formato de casos do plugin-eval é separado do arquivo evals/evals.json que o plugin skill-creator usa para avaliações de skills.
Pressione Enter ou Espaço para virar o cartão. Use as setas esquerda e direita para navegar entre os cartões.Termo exibido.
1 / 7

Teste-se

0/6
  1. Um caso marca 1.00 WITH e 1.00 W/OUT. O que isso diz?
  2. Seu job de CI roda `claude plugin eval . --json results.json` sem outros flags e sai com 1 embora todo caso tenha marcado 0.9. Por quê?
  3. Quais tipos de grader não custam nada extra para rodar?
  4. O Δ é negativo, mas o grader skill-fired passou em toda execução do braço com plugin. O que a orientação oficial diz para checar primeiro?
  5. Uma ferramenta MCP simulada numa execução recebe uma chamada cuja entrada viola o bloco `expect:` do mock. O que acontece?
  6. A qual destas coisas o agente sob teste tem acesso durante uma execução?

Fontes e leitura adicional​

Próximo​

  • Plugins & Marketplaces — empacote o que você acabou de testar e publique quando a suíte estiver verde
  • Skills — o campo description é o que um grader skill-fired falhando está dizendo para você corrigir
  • Reviewing Third-Party Code — antes de rodar o plugin de outra pessoa (e os hooks dele) por um eval na sua máquina