Prologue
In my previous post, I experimented with implementing a poor man’s testing tool for my Claude Code configuration. One option I explored while researching the space was promptfoo. At the time of the initial research, promptfoo did support Claude Code as a provider, but with a catch. It only supported API billing, which was undesirable in my case.
In this post, I will explore the options for using promptfoo with Claude Code and a subscription:
- Use
anthropic:claude-agent-sdkthrough a local Claude Code login, withapiKeyRequired: false(Option 1). - Implement a custom provider (Option 2).
The setup
Everything lives in one folder:
| |
promptfoo and the Agent SDK get installed in the same folder:
| |
The test
I am reusing the music library example from the previous post. My context says the library lives in ~/Documents/Music. Asking for it should need no tools at all. This is tests.yaml:
| |
icontainschecks the answer.- The
javascriptassertion checks how the answer was produced. It reads the tool calls that the provider reports.
Both options run this same file.
Option 1: the built-in provider
This is builtin.yaml:
| |
apiKeyRequired: falseskips the API key check, and the SDK uses my Claude Code login.working_diris the directory the session runs in.setting_sourcesloadsCLAUDE.mdand my settings. By default it loads neither.promptsandteststie the provider to the question intests.yaml.
To run it:
| |
--no-cache matters. Without it promptfoo may replay a cached answer, and a configuration change goes unnoticed.
Option 2: a custom provider
A promptfoo Python provider is a file with a call_api function. Mine runs claude -p and collects every tool call along the way. This is claude_cli.py:
| |
The built-in provider reports tool calls under the same metadata.toolCalls key, which is why one test file works for both.
custom.yaml points to the provider file:
| |
To run it:
| |
The results
I ran the test three times against each, with ANTHROPIC_API_KEY unset. The last row is the same test in cctest, the tool from the previous post, with tools available:
| Provider | Passed | Tool calls | Time per run |
|---|---|---|---|
Built-in, with setting_sources | 3 of 3 | 0 | 5 to 6s |
| Built-in, defaults | 0 of 3 | 6 to 15 | 36 to 66s |
Custom (claude CLI) | 3 of 3 | 0 | 9 to 18s |
| cctest | 3 of 3 | 0 | 9 to 10s |
Both options work on my subscription, and the built-in one is faster.
With the defaults it knows nothing about me. It went through my mpd and XDG configuration, answered ~/Music once and gave up twice.
One difference remains. The built-in provider only allows the MCP servers you configure for it, while the CLI starts everything I actually run.
The question matters
In the previous post the question was “where does my music live?”. One run proves little, so I used --repeat 3 to run each case three times. With that wording, the same test is flaky:
| |
The path was in context every time. The tool calls were not looking for it.
- On Opus 5.5, the model opened the memory file behind the line that holds the path. Three times out of three.
- On Fable 5.2, it ran
lson the directory to check that it exists. One time out of three.
An open question invites more detail and a quick check. A narrow one gets the path and nothing else. If the test is about recall, ask for exactly the fact.
Closing thoughts
promptfoo is meant to also work with Codex and OpenCode. I am not sure about the status of Antigravity. Maybe that is something we should see in a future post.
Using JavaScript to check the actual response is powerful, but not ideal for people who are not that technical. This is a thing that my custom tool does better. Maybe it makes sense to rewrite my tool on top of promptfoo. That is also something worth exploring in a future post.
Till next time!