| title | Eval Runner - Guide utilisateur | |||||||
|---|---|---|---|---|---|---|---|---|
| type | documentation | |||||||
| version | 2.0 | |||||||
| created | 2026-03-30 | |||||||
| updated | 2026-03-30 | |||||||
| domain | tooling | |||||||
| tags |
|
|||||||
| summary | Guide d'installation, configuration et usage de l'Eval Runner, outil de calibration des skills, commands et hooks Claude Code via execution reelle (e2e) et analyse de trajectoire. |
Outil de calibration des skills, commands et hooks Claude Code par execution reelle.
Chaque eval lance Claude Code dans un environnement isole avec des fichiers de test (fixtures), capture la trajectoire complete (outils utilises, arguments, ordre), et verifie l'etat final des fichiers.
Inspire de la methodologie Anthropic "Demystifying Evals for AI Agents" :
- Binary pass/fail
- Grade outcomes, not paths (mais capture la trajectoire pour le debug)
- Deux axes : trigger (routing) et e2e (execution reelle)
- Bun >= 1.0
- Claude Code CLI installe et authentifie (
claude --version)
bun installAucune configuration supplementaire requise. Le runner utilise l'authentification Claude Code deja presente sur la machine.
Pour voir le runner fonctionner rapidement, utilisez un eval de type hook — aucun appel LLM, execution quasi-instantanee.
- Creez un fichier
_evals/hooks/demo.eval.yaml:
name: demo
type: hook
target: .claude/hooks/demo.sh
cases:
- scenario: "Commande reussie, pas de contexte ajoute"
input:
tool_result:
exit_code: 0
stdout: "ok"
stderr: ""
expect:
has_context: false- Lancez le runner :
bun run eval demoVous devriez voir PASS demo en sortie console. Pour un eval skill/command complet avec fixtures et outcomes, voir la section Format YAML des test cases.
| Variable | Defaut | Description |
|---|---|---|
EVAL_MODEL |
opus |
Modele Claude Code a utiliser (haiku, sonnet, opus) |
Le modele peut aussi etre specifie par scenario dans le YAML (model: sonnet).
# Executer tous les evals (trigger + e2e + hook)
bun run eval
# Filtrer par nom
bun run eval ts-fixer
# Un axe seulement
bun run eval --axis trigger
bun run eval --axis e2e
# Afficher les trajectoires et details
bun run eval --verbose
# Combiner les options
bun run eval ts-fixer --axis e2e --verbose| Option | Type | Defaut | Description |
|---|---|---|---|
[name] |
positional | - | Filtre par nom exact de l'eval |
--axis |
trigger | e2e |
tous | Axe d'evaluation a executer |
--trials |
1-10 |
1 |
Nombre de repetitions par scenario |
--verbose |
boolean | false |
Affiche les trajectoires et outputs |
| Code | Signification |
|---|---|
0 |
Tous les evals passent |
1 |
Au moins un eval echoue |
2 |
Erreur CLI (option invalide) |
Les fichiers d'eval sont dans _evals/ avec l'extension .eval.yaml.
_evals/
├── skills/
│ └── ts-fixer.eval.yaml
├── commands/
│ └── tdd.eval.yaml
├── hooks/
│ └── ts-fixer-hook.eval.yaml
└── results/
└── 2026-03-30T14-30-00_ts-fixer.json
Les skills et commands partagent le meme schema avec deux sections : trigger et e2e.
name: mon-skill # Identifiant unique
type: skill # "skill" ou "command"
target: .claude/skills/mon-skill/SKILL.md # Fichier cible (lu pour trigger eval)
# ── Trigger eval ──
# Teste si la description du skill route correctement les requetes
trigger:
positive: # Requetes qui DOIVENT matcher
- query: "Fix the TypeScript errors"
note: "Explication optionnelle"
negative: # Requetes qui NE DOIVENT PAS matcher
- query: "Fix the CSS styling"
note: "CSS, pas TypeScript"
# ── E2E eval ──
# Lance Claude Code pour de vrai dans un dossier temp avec des fixtures
e2e:
- scenario: "Fix type assignment error"
fixture: # Fichiers crees dans le dossier temp
src/app.ts: |
const greeting: number = "hello world";
console.log(greeting);
tsconfig.json: |
{ "compilerOptions": { "strict": true, "noEmit": true } }
prompt: "Fix the TypeScript errors in this project"
model: haiku # Optionnel (defaut: EVAL_MODEL ou opus)
outcomes: # Verifications sur l'etat final des fichiers
- type: file-not-contains
file: src/app.ts
value: "as any"
- type: file-not-contains
file: src/app.ts
value: "@ts-ignore"
workflow: # Verifications sur la trajectoire d'outils
match_mode: superset
expected_tools:
- name: Read
args_contain:
file_path: "app.ts"
- name: Edit
args_contain:
file_path: "app.ts"
after: Read # Edit doit venir APRES Read
forbidden_tools:
- Write # Ne doit PAS reecrire le fichier entierLes hooks sont testes par execution directe du script shell. Pas de Claude Code implique.
name: mon-hook
type: hook
target: .claude/hooks/mon-hook.sh
cases:
- scenario: "Commande echouee avec erreurs TS"
input:
tool_result:
exit_code: 1
stdout: "error TS2322: Type 'string' is not assignable"
stderr: ""
expect:
has_context: true
context_contains: "TypeScript"
- scenario: "Commande reussie"
input:
tool_result:
exit_code: 0
stdout: "Build successful"
stderr: ""
expect:
has_context: falseEvalue la precision du routing : est-ce que la description du skill attire les bonnes requetes ?
Comment ca marche : Claude Code recoit un prompt "est-ce que cette requete devrait declencher ce skill ?", repond OUI/NON. Pas de tool use, pas de fichiers — juste du texte.
Metriques :
| Metrique | Formule | Interpretation |
|---|---|---|
| Precision | TP / (TP + FP) | % des triggers qui sont corrects |
| Recall | TP / (TP + FN) | % des requetes pertinentes detectees |
| F1 | 2 * P * R / (P + R) | Equilibre precision/recall |
Evalue le comportement reel de Claude Code : est-ce qu'il utilise les bons outils et produit le bon resultat ?
Comment ca marche :
1. Cree un dossier temp avec les fichiers de la fixture
2. Lance: claude -p "<prompt>" --bare --cwd /tmp/eval-xxx --model haiku
3. Claude Code lit les fichiers, fait des edits, lance des commandes...
4. Capture la trajectoire (sequence de tool calls)
5. Verifie l'etat final des fichiers (outcomes)
6. Verifie la trajectoire (workflow)
7. Nettoie le dossier temp
Types d'outcomes (verifications sur les fichiers apres execution) :
| Type | Description |
|---|---|
file-contains |
Le fichier contient une sous-chaine |
file-not-contains |
Le fichier NE contient PAS une sous-chaine |
file-matches |
Le fichier matche une regex |
file-exists |
Le fichier existe |
file-not-exists |
Le fichier n'existe pas |
bash-succeeds |
La commande bash retourne exit 0 |
bash-fails |
La commande bash retourne exit != 0 |
bash-output-contains |
La sortie de la commande contient une sous-chaine |
Workflow — modes de matching (verifications sur la trajectoire d'outils) :
| Mode | Verifie | Quand l'utiliser |
|---|---|---|
strict |
Memes outils, meme ordre, memes args | Workflow tres precis |
unordered |
Memes outils dans n'importe quel ordre | Quand l'ordre n'importe pas |
subset |
L'agent n'utilise QUE les outils listes | Verifier qu'il ne fait pas trop |
superset |
L'agent utilise AU MOINS les outils listes | Le minimum requis (recommande) |
Workflow — contraintes d'ordre :
expected_tools:
- name: Edit
after: Read # Edit doit apparaitre APRES Read dans la trajectoireWorkflow — outils interdits :
forbidden_tools:
- Write # Si Claude Code utilise Write au lieu de Edit, le test echoueEvalue les hooks shell par execution directe. Pas de Claude Code, pas de LLM.
Comment ca marche :
- Le script hook est execute avec mock JSON en stdin
- Le stdout est parse comme JSON
- Verification de
additionalContext(presence + contenu)
━━━ ts-fixer (skill) ━━━
Trigger Eval
Positive: 3/3 (100%)
Negative: 3/3 (100%)
Precision: 100% Recall: 100% F1: 100%
E2E Eval (cost: $0.0842)
PASS Fix type assignment error (8523ms, $0.0521)
Trajectory: Read → Read → Edit → Bash
PASS [file-not-contains] src/app.ts not contains "as any"
PASS [file-not-contains] src/app.ts not contains "@ts-ignore"
PASS Workflow checks passed
FAIL Fix missing property error (12341ms, $0.0321)
Trajectory: Read → Write
PASS [file-not-contains] src/user.ts not contains "as any"
FAIL Forbidden: Write
Pass rate: 50%
Duration: 21000ms
═══ Summary ═══
FAIL ts-fixer
Total e2e cost: $0.0842
0 passed, 1 failed
Chaque run produit un fichier horodate dans _evals/results/ :
{
"name": "ts-fixer",
"type": "skill",
"timestamp": "2026-03-30T14:30:00.000Z",
"config": { "model": "haiku", "trials": 1 },
"trigger": {
"positive": { "total": 3, "passed": 3, "rate": 1.0 },
"negative": { "total": 3, "passed": 3, "rate": 1.0 },
"precision": 1.0, "recall": 1.0, "f1": 1.0
},
"e2e": {
"scenarios": [
{
"scenario": "Fix type assignment error",
"trajectory": [
{ "tool_name": "Read", "tool_args": { "file_path": "/tmp/eval-xxx/src/app.ts" } },
{ "tool_name": "Edit", "tool_args": { "file_path": "/tmp/eval-xxx/src/app.ts", "..." : "..." } }
],
"outcomes": [
{ "type": "file-not-contains", "description": "...", "passed": true }
],
"workflow": {
"tool_match": { "passed": true, "reason": "All required tools present" },
"forbidden_check": { "passed": true, "violations": [] },
"ordering_check": { "passed": true, "violations": [] }
},
"cost_usd": 0.0521,
"duration_ms": 8523,
"passed": true
}
],
"pass_rate": 1.0,
"total_cost_usd": 0.0521
},
"duration_ms": 21000
}scripts/
├── eval-runner.ts # Entry point CLI + orchestration
└── eval/
├── types.ts # Types TypeScript + schemas Zod
├── config.ts # Config (model, dirs)
├── claude-run.ts # Helper claude -p (shared trigger/e2e)
├── loader.ts # Discovery YAML + parsing + validation
├── trigger.ts # Trigger eval via claude -p
├── e2e.ts # E2E eval (fixtures, trajectoire, outcomes)
├── hooks.ts # Hook eval (shell exec)
└── report.ts # Rendu console ANSI + ecriture JSON
Tout passe par claude -p (le CLI Claude Code en mode non-interactif). Aucune API externe, aucune cle API, aucun .env requis.
| Symptome | Cause probable | Solution |
|---|---|---|
claude: command not found |
CLI Claude Code non installe | Installer Claude Code, puis verifier avec claude --version |
claude auth status indique not logged in |
Session expiree ou absente | Lancer claude auth login |
bun: command not found |
Bun non installe | curl -fsSL https://bun.sh/install | bash |
| Eval timeout / scenario bloque | Depasse le timeout_ms de claudeRun |
Defaut 5 minutes (300000 ms). Ajuster le timeout_ms dans claude-run.ts ou reduire la taille de la fixture |
| Warning MCP dans le sandbox | Voir e2e.ts:217 — le sous-processus Claude Code ne charge pas tous les MCP dans le dossier temp |
Generalement sans impact sur l'eval, peut etre ignore |
| Erreur de validation YAML (Zod) | Le fichier .eval.yaml ne respecte pas le schema |
Consulter les sections Schema Skill / Command et Schema Hook plus haut |
| Runs e2e plus longs qu'attendu | Modele Opus par defaut | Passer EVAL_MODEL=haiku en variable d'env ou ajouter model: haiku dans le scenario |
- Creer un fichier
_evals/skills/mon-skill.eval.yaml(oucommands/,hooks/) - Remplir le schema YAML correspondant au type
- S'assurer que le
targetpointe vers un fichier existant (pour trigger eval) - Pour les e2e : definir les fixtures (vrais fichiers de test)
- Lancer
bun run eval mon-skill --verbosepour tester - Ajuster les fixtures, outcomes et workflow selon les resultats
- Fixtures minimalistes : le moins de fichiers possible pour reproduire le cas
- Outcomes deterministes : preferer
file-not-containsetbash-succeedsaux checks fragiles supersetpar defaut : verifier le minimum requis, pas le chemin exactafterpour les contraintes d'ordre : Read avant Edit, pas de strict ordering globalforbidden_toolspour les anti-patterns :WritequandEditsuffit- 20-50 test cases par cible (recommandation Anthropic)
- Equilibrer positifs et negatifs dans les triggers
- Haiku pour le dev, Sonnet pour la CI : calibrer avec Haiku (rapide), valider avec Sonnet
Les resultats JSON contiennent un champ cost_usd (et total_cost_usd agrege). Cette valeur est calculee par Claude Code a partir des tarifs API et represente une consommation equivalente. Avec un abonnement Claude Pro ou Max, les runs sont couverts par l'abonnement — pas de facturation par eval. La valeur reste utile pour comparer l'intensite relative entre scenarios et modeles.
Released under the MIT License. See LICENSE.