The tax-eval-harness evaluation framework was generalized from a tax-specific implementation to a skill-agnostic evaluation harness. Any skill can now ship evals as a first-class part of its package.
| File | Purpose |
|---|---|
src/tax_eval/eval_defs.py |
EvalDefinition dataclass, YAML loading, auto-discovery |
src/tax_eval/eval_builder.py |
Converts definitions → SDK hooks + post-run evals |
src/tax_eval/skill_agent.py |
Generic SkillAgent + BatchSkillRunner + CLI |
.claude/skills/tax-analyzer/evals.yaml |
Tax evals migrated to declarative YAML |
.claude/skills/tax-analyzer/evals/pii_check.py |
Example function eval |
| File | Changes |
|---|---|
src/tax_eval/skill_loader.py |
Added evals_dir, evals_yaml_path, has_evals, build_generic_prompt() |
src/tax_eval/__init__.py |
Added new exports |
pyproject.toml |
Added pyyaml dep, skill-eval CLI entry point |
All existing code untouched: eval_hooks.py, tax_agent.py, run_example.py. Legacy API still works.
| Mode | Trigger Field | Default Event | Default Layer |
|---|---|---|---|
| Descriptive | prompt |
stop |
semantic |
| Script | script |
post_run |
deterministic |
| Function | function |
pre_tool (if tool set) / post_run |
deterministic |
| regex_deny | check: regex_deny |
pre_tool |
deterministic |
| path_restrict | check: path_restrict |
pre_tool |
deterministic |
Smart defaults minimize YAML boilerplate. Explicit values always override defaults.
.claude/skills/any-skill/
├── SKILL.md
├── evals.yaml # Declarative eval definitions
├── evals/ # Auto-discovered scripts/functions
│ ├── validate_output.py # Must export evaluate(context) -> (bool, str)
│ └── check_format.sh # Exit 0 = pass, non-zero = fail
├── references/
└── scripts/
Auto-discovery: Any .py with evaluate() or .sh in evals/ is found automatically, even without evals.yaml. YAML definitions take precedence over auto-discovered ones with the same name.
The current design is 1:1 — one SkillAgent runs one skill with its evals. Hooks are registered globally on the SDK client.
A plugin or orchestrating agent may need multiple skills simultaneously (e.g., a financial-analysis plugin using tax-analyzer + budget-planner + report-generator). Each skill ships its own evals, so:
- Evals must be scoped to the skill that defined them (tax write-restriction shouldn't block the report generator)
- Evals must activate/deactivate as the agent switches between skills
- Hooks need skill context (which skill is currently executing)
Explored the SDK internals and found:
- No
SkillDefinitiontype — skills exist only at the Cowork/Claude Code app level as filesystem conventions (SKILL.md + manifest.json) AgentDefinition— hasdescription,prompt,tools,modelbut no hooks fieldSdkPluginConfig— justtype+path, a pointer not a definition- Global hook registry — no per-skill isolation
SubagentStart/SubagentStophooks — lifecycle boundaries that could serve as scoping anchorssession_idin hooks — available but not skill-scoped
SkillDefinition — extends AgentDefinition semantics with eval/hook support:
@dataclass
class SkillDefinition:
name: str
agent: AgentDefinition # prompt, tools, model
eval_definitions: list[EvalDefinition]
hooks: dict[str, list] # skill-scoped hooks
root_dir: PathPluginRunner — multi-skill orchestrator:
class PluginRunner:
skills: dict[str, SkillDefinition]
active_skill: str | None
# Uses SubagentStart/SubagentStop to scope evals:
# - SubagentStart → activate that skill's hooks
# - SubagentStop → deactivate, record results
# - Global hooks (e.g., PII check) always activeKey design decisions needed:
- How does the orchestrating agent decide which skill to invoke? (explicit routing vs. LLM choice)
- Should skills share eval reports or have isolated ones?
- How to handle cross-skill evals (e.g., "final output incorporates all skills' work")?
- Should this be a wrapper on top of the SDK, or a PR to the SDK itself?
┌─────────────────────────────────────────────┐
│ Cowork App / Claude Code │
│ Rich skill packages: SKILL.md, references, │
│ scripts, evals, manifest.json │
│ BUT: no programmatic representation │
├─────────────────────────────────────────────┤
│ Claude Agent SDK (Python) │
│ AgentDefinition: prompt, tools, model │
│ SdkPluginConfig: type, path │
│ NO: hooks, evals, references, scripts │
└─────────────────────────────────────────────┘
Our SkillPackage + EvalDefinition pipeline is essentially constructing what a native SDK SkillDefinition would provide. The natural path forward is either:
- Build it as a library layer on top of the SDK (what we can do now)
- Propose it as an SDK primitive (requires Anthropic SDK team alignment)
77 tests passing across all test files. Fixed a Python module caching issue where importlib.import_module returned stale cached modules across test fixtures with different tmp_path values — solved by using importlib.util.spec_from_file_location with unique module names.
uv run pytest -v # All 77 pass
uv run skill-eval inspect # CLI works
uv run tax-eval inspect # Legacy CLI works