Skip to content
My Guidelines
Guides
In this guide (13 items)

Evals for Agent Config

Evals — Regression Testing for Agent Configuration

MANDATORY FOR ALL AI AGENTS: CLAUDE.md, ~/.claude/rules/, ~/.claude/skills/ and ~/.claude/hooks/ are production configuration, not notes. They MUST be gated by evals exactly like code is gated by tests. You MUST NOT merge a change to any of those paths without a green eval run. Every repeated agent mistake MUST become a permanent eval case before the fix is called done.

Fifty-plus skills, twenty hooks and two large CLAUDE.md files are a system with no tests. Change one skill description and you silently change which skill fires for a hundred future prompts. Evals are the only feedback loop that catches that.

Companion files: hooks.md (§4 gates evals in CI), ai-sdlc.md (§3 Test stage), testing.md (code-level testing — different target).


1. What an eval is

A frozen prompt plus machine-checkable assertions about the agent’s answer or the files it left behind.

PropertyRule
Deterministic checkAssertions are regex or shell exit codes. Never “looks reasonable”.
One behaviourOne case asserts one rule. A case that fails must name the broken rule.
RealCases come from prompts actually issued and mistakes actually made — never invented.
CheapTarget under 60s per case. Long agentic tasks belong in UAT, not the eval suite.
TraceableEvery case declares the rule or skill file it defends.

Size: 20–50 cases. Below 20 the suite proves nothing; above 50 it stops being run.


2. Case format

One JSON file per case in ~/.claude/evals/cases/<id>.json.

{
"id": "attribution-no-trailer-non-codehunters",
"defends": "rules/git-attribution.md",
"severity": "critical",
"cwd": "fixtures/repo-client",
"prompt": "Commit the staged change. Message: fix the token expiry off-by-one in auth middleware.",
"checks": [
{ "type": "output_not_matches", "pattern": "(?i)co-authored-by:\\s*claude" },
{ "type": "output_not_matches", "pattern": "(?i)generated with .*claude code" },
{ "type": "output_matches", "pattern": "(?i)^fix\\(auth\\)|fix:" }
]
}

Fields

FieldMeaning
idKebab-case, unique, equals the filename.
defendsPath (relative to ~/.claude/) of the rule, skill or guideline this case protects.
severitycritical (blocks merge) | normal (blocks merge) | advisory (reported, never blocks).
cwdFixture directory the agent runs in. Relative to evals/. Optional.
setupShell run before the case. Use to build a dirty worktree, a populated table, a stale changeset. Optional.
promptVerbatim user prompt. Do NOT hint at the rule — that tests the prompt, not the config.
checksAll must pass.

Check types

TypePasses when
output_matchesAgent’s final text matches the regex.
output_not_matchesIt does not. The workhorse — most rules are prohibitions.
file_matches / file_not_matchespath + pattern against a file the agent wrote.
file_exists / file_absentpath exists or does not.
command_succeedscommand exits 0 in cwd (compile, spotlessCheck, git log | grep -c).

The negative check is the important one. Rules in ~/.claude/rules/ are overwhelmingly “never do X”; a suite of only positive checks will pass while every prohibition rots.


3. Running

Terminal window
~/.claude/evals/run.sh # whole suite
~/.claude/evals/run.sh attribution # only ids matching a substring
~/.claude/evals/run.sh --severity critical

The runner drives the agent non-interactively:

Terminal window
claude --bare -p "$prompt" --output-format json

--bare skips hook, skill, plugin, MCP and CLAUDE.md auto-discovery. That is correct for cases testing an explicitly loaded rule and wrong for cases testing discovery (does the right skill fire at all?). Declare it per case with "bare": false when discovery is the thing under test — discovery regressions are the most common and the most invisible.

Credentials

The runner needs a credential with balance. The ambient ANTHROPIC_API_KEY on this machine is a zero-balance key — the interactive CLI runs on the subscription via keychain, which the non-interactive -p path does not inherit. A case that cannot reach the API reports ERROR, never FAIL, and the suite exits 2 marked INCONCLUSIVE.

That distinction is the whole point. A credential failure that renders as a red FAIL teaches you to ignore red, and an eval suite you have learned to ignore is worse than none. Fix the credential, re-run, then read the result.

Results land in ~/.claude/evals/results/<iso-date>.json. Keep them; the trend line is the signal, a single run is noise.


4. The CI gate

Configuration changes merge only on a green suite.

.github/workflows/evals.yml
name: agent-config-evals
on:
pull_request:
paths:
- 'CLAUDE.md'
- '.claude/**'
- 'rules/**'
- 'skills/**'
- 'hooks/**'
schedule:
- cron: '0 6 * * 1' # weekly drift check against model updates
jobs:
evals:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install -g @anthropic-ai/claude-code
- run: ./evals/run.sh --severity critical
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

The scheduled run matters as much as the PR run. Your configuration can break without you touching it, because the model underneath changed. A weekly cron is how you find out in a pull request instead of in a commit that shipped an AI trailer to a client repo.

Gate policy

Suite stateMerge
All critical pass, ≥90% overallAllowed
Any critical failsBlocked. No override.
normal regressionBlocked; author fixes the config or justifies the new behaviour by editing the case in the same PR.
advisory regressionReported in the PR comment, never blocks.

Editing a case to make it pass is a legitimate move only in the PR that deliberately changes the rule, and the case diff must be reviewed harder than the config diff. Silently loosening a check is the eval-suite equivalent of deleting a failing test.


5. Incident to eval

The rule: a mistake seen twice is a missing eval case, not a missing reminder.

  1. Agent breaks a rule (or a human catches it in review).
  2. Fix the immediate damage.
  3. Write the case that reproduces it — before editing the rule text.
  4. Confirm the case FAILS against current config. A case that passes on first write is testing nothing.
  5. Strengthen the rule / skill / hook until the case passes.
  6. Commit case and config together.

Step 4 is non-negotiable and is the step people skip. Red-Green-Refactor applies to configuration exactly as it applies to code (testing.md §2).

This retroactively converts scar tissue into coverage. The two forced history rewrites over AI attribution trailers are the canonical example: the rule already existed in rules/git-attribution.md and was still violated three separate times, because prose in a file is advisory and nothing measured whether it was working.


6. Starter battery

The initial suite is derived from failures that actually happened, grouped by the rule they defend.

GroupDefendsCases
Attributionrules/git-attribution.mdNo trailer outside Codehunters; exemption honoured inside it; never version CLAUDE.md / .claude/ / .mcp.json; no tool name in PR body.
Architecture routingCLAUDE.md, project CLAUDE.mdForgeOS gets Modulith DDD, not hexagonal layers; a fresh JVM service gets hexagonal; no @Transactional on a use case; no cross-module table access.
Code stylerules/coding-style.mdJsonMapper never ObjectMapper; function over 10 lines is extracted; over 3 params becomes a command object; no Kotlin !!.
Toolingrules/modern-cli.mdrg over grep -r; fd over find; native Read over cat for analysis.
Migrationsskills/liquibase-immutable-changesetsCommitted changeset is never edited — forward fix only; NOT NULL on a populated table becomes expand/contract.
Testingrules/testing.mdFeature without a test is refused; test precedes implementation.
Securityrules/security.mdNo hardcoded secret; no string-concatenated SQL.
Stack factsproject CLAUDE.mdSpring AI 1.1 on Boot 4 is rejected as incompatible.

Severity: attribution and security are critical — they have a blast radius outside the repo. Style and tooling are normal. Stack facts are advisory; they age.


7. Metrics

HorizonMeasureSource
LeadingSuite pass rate per weekevals/results/*.json
LeadingTime from incident to committed caseincident note → case commit
LaggingRule violations reaching a PRreview findings citing a rule
LaggingRule violations reaching mainhistory rewrites, hotfixes

The target for the lagging pair is zero, and it is reachable: these are deterministic rules, not judgement calls.


8. Anti-patterns

Anti-patternWhy it failsInstead
Hinting the rule in the promptTests the prompt, not the configurationWrite the prompt a real user would write
LLM-judged “is this good?” checksNon-deterministic gate, flaky mergesRegex or exit code
Only positive assertionsEvery prohibition rots undetectedNegative checks for every “never” rule
Loosening a check to get greenDeletes coverage silentlyFix the config, or change the rule deliberately in the same PR
Suite that takes 40 minutesStops being run, then stops being trueCap at 50 cases, 60s each, parallelise the runner
Writing a case that already passesProves nothingConfirm red first (§5.4)
Only running on PRsModel updates break config with no diffWeekly cron (§4)

9. Quick reference

Terminal window
# run everything
~/.claude/evals/run.sh
# run the blocking subset (what CI gates on)
~/.claude/evals/run.sh --severity critical
# add a case after an incident
cp ~/.claude/evals/cases/_template.json ~/.claude/evals/cases/<id>.json
$EDITOR ~/.claude/evals/cases/<id>.json
~/.claude/evals/run.sh <id> # MUST fail here
$EDITOR ~/.claude/rules/<rule>.md # strengthen
~/.claude/evals/run.sh <id> # now green
# trend
jq -r '[.date, .passed, .total] | @tsv' ~/.claude/evals/results/*.json