TL;DR
claude plugin eval .corre cada caso tres veces con tu plugin cargado y otras tres sin nada, y te devuelveWITH,W/OUTyΔ. La única columna que dice que tu plugin aportó algo es la Δ: un caso con 1.00 en las dos es un caso que Claude ya resolvía solo. Y cuidado con meterlo en el CI tal cual, porque--thresholdmiraWITHy nunca la Δ.
Cuando pruebas un plugin lo pruebas a una sola banda. Lanzas la petición, tu skill se dispara, Claude responde bien y cierras la sesión convencido de que funciona. Lo que no has visto nunca es la otra mitad del experimento: qué habría contestado Claude con tu plugin desinstalado. Y sin esa mitad no sabes lo único que importa, que es si tu plugin causó el resultado o si simplemente estaba delante cuando ocurrió.
Esto no va de si tu plugin carga bien. Para eso está claude plugin validate. Un plugin puede cargar perfecto, hacer exactamente lo que escribiste, y no cambiar ni una coma de lo que Claude habría contestado igual. Un plugin así es peso muerto, igual que los que ni siquiera llegas a usar.
Cómo funciona
claude plugin eval monta el experimento que a mano no puedes montar. Por cada caso abre una sesión limpia y no interactiva con solo tu plugin cargado, le manda el prompt y puntúa el resultado con los graders que tú has escrito. Después repite exactamente lo mismo con cero plugins.
Cada brazo corre tres veces por defecto, porque una sola tirada de un agente no determinista no dice nada. Un caso son seis sesiones, y de ahí salen tres números: WITH con tu plugin, W/OUT sin él, y Δ, que es la resta.
Lo que sale por pantalla
Una suite de dos casos sobre un plugin mínimo, una sola skill que escribe mensajes de commit:
CASE WITH W/OUT Δ RUNS COST NOTES
first-case 1.00 0.00 +1.00 6 $0.53
plain-request 1.00 1.00 0.00 6 $0.46
2 case(s) · mean Δ +0.50 · 109s · $0.98
Las dos filas sacan un 1.00 limpio en WITH. Mirando solo esa columna, el plugin es perfecto en los dos casos.
La Δ cuenta otra cosa. En first-case el plugin hizo el trabajo entero. En plain-request no hizo nada: Claude resolvía igual de bien la petición sin tenerlo delante. Y no es que la skill no llegara a entrar, porque en las tres tiradas con plugin salió esto:
✓ skill-fired [with-only, not scored]: Skill called 1x (expected 1..∞)
Se disparó, se ejecutó, y dio exactamente lo mismo. Ese es el caso que hay que borrar o reescribir, y es invisible si solo miras la nota.
Montarlo en tu plugin
1. Que Claude escriba la suite
Desde la raíz del plugin, la que tiene el plugin.json:
claude plugin eval init
Lee tu plugin, te pregunta qué es un buen resultado, propone prompts que deberían y que no deberían dispararlo, y escribe un directorio por caso bajo evals/. Si prefieres ver los archivos por dentro, claude plugin eval init --bare primer-caso deja la plantilla en blanco sin gastar una llamada al modelo.
2. El caso es un prompt con frontmatter
En evals/primer-caso/prompt.md, el cuerpo es literalmente lo que recibe Claude. Escríbelo como lo pediría un usuario, sin nombrar tu skill:
---
max_turns: 6
allowed_tools: [Skill]
---
Escríbeme el mensaje de commit de este cambio: he renombrado getUser a fetchUser y he actualizado las tres llamadas.
3. Los graders son un archivo cada uno
Uno sobre el resultado, en graders/criteria.md, y otro sobre el camino, que comprueba que fue tu skill quien lo produjo. La documentación recomienda esa pareja: uno mira qué salió y el otro mira quién lo hizo.
---
type: llm
---
PASS si la respuesta es una única línea de Conventional Commits con la forma `type(scope): resumen`.
FAIL si es prosa, una lista de opciones, o un mensaje que no empieza por un tipo de Conventional Commits.
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:[\w-]+:)?commit-msg"'
---
4. Correrlo
claude plugin eval .
La primera vez te pregunta Trust this plugin directory?, porque va a cargar y ejecutar ese plugin en tu máquina con tu credencial.
Dos cosas que te van a morder
El grader de la skill no puntúa. Es el [with-only, not scored] de arriba. Un grader que comprueba "se invocó mi skill" no puede pasar nunca en el brazo sin plugin, así que contarlo hundiría ese brazo y te inflaría la Δ con una mentira. Claude Code lo saca de la puntuación en los dos brazos y lo deja como indicador. Afecta a todos los tool_used sobre Skill y a cualquier grader que marques arm: with-only.
El CI se pone verde con un plugin inútil. --threshold compara contra la columna WITH, nunca contra la Δ. El caso plain-request de arriba, con su Δ de 0.00, devuelve exit code 0 con el umbral por defecto de 1.0. Si quieres que una caída de aporte rompa el build, la Δ la tienes que leer tú en el aggregate-result.json, donde sale como meanDelta.
Cuando la Δ salga cerca de cero y el grader de la skill en rojo, el diagnóstico casi siempre es el mismo: la description de tu skill no se dispara con esa forma de pedir las cosas. Es la misma regla 2 de las cinco que publica Anthropic, ahora con un número delante. Ese tip usa el evals/evals.json de skill-creator, que es un formato distinto del que lee este comando.
Los seis tipos de grader
| Tipo | Pasa cuando | Cuesta |
|---|---|---|
regex |
El patrón aparece (o no, con match: not_contains) en el objetivo |
Gratis |
tool_used |
El número de llamadas a esa tool cae entre min y max |
Gratis |
tool_order |
La primera llamada de before va antes que la de after |
Gratis |
file_exists |
Un archivo creado durante la tirada casa con el glob path |
Gratis |
llm |
Un modelo juez vota PASS en dos de tres votos sobre tu rúbrica | Llamada al modelo |
baseline |
El juez ve la tirada al menos tan buena como un transcript de referencia | Llamada al modelo |
Documentación oficial: Test plugins with evals
Requisitos: Claude Code 2.1.269 o superior. Las tiradas y los graders llm llaman al modelo con tu credencial y cuentan contra tu plan, y los costes de la tabla son una estimación a precio de lista.