---
title: "Claude Code Explore Agent Doesn't Run Haiku. Mine Ran Opus."
date: 2026-09-09
description: "I believed Claude Code's Explore agent ran Haiku. 800 subagent transcripts on my laptop say Opus, every time. Here's how to check yours, and how to steer it."
url: /posts/claude-code-explore-agent-haiku/
tags: ai-tools, claude-code, developer-workflow, tokens
---


I had this long-held belief that Claude Code used Haiku for the Explore agent. I don't know where I picked it up. Tribal knowledge on Reddit, maybe? In any case, it wasn't (or it was, and isn't any longer).

And it's been checkable the whole time, on my machine.

Here's what finally made me look. Spotify's engineering blog published a piece last week titled ["Portal by Spotify cut my Claude Code token usage by 90%."](https://engineering.atspotify.com/2026/9/portal-by-spotify-cut-my-claude-code-token-usage-by-90) I read it twice. The diagnosis is exactly right.

The author's framing is the best sentence in the post: *"Most of what an AI coding agent does for me isn't thinking. It's I/O."*

When Claude Code reads eleven files to answer "where is this defined," you are paying frontier-model prices to do `grep`. The fix Spotify built is a three-layer system: `PreToolUse` hooks that block reads over 350 lines, bash wrappers that call their Portal CLI, and skills that teach the model when to use them. Big reads get routed to Gemini 2.5 Flash. They measured about 90% mean savings on bulk reads against a Java monorepo.

The post is unusually honest about its own limits. It says plainly that delegation can't handle editing, can't handle reasoning (it missed a thread-safety bug in testing), and adds 10 to 30 seconds of latency per call. That's a more candid limitations section than most vendor engineering blogs manage.

But before I stood up an ephemeral runtime to solve this, I wanted to know what my agent was actually spending money on.

**The short version:**

- **The claim is stale.** Explore's model is `"inherit"`. It runs whatever your main session runs, capped at Opus. Not Haiku.
- **You can verify this yourself.** Claude Code logs the model *and* the effort level for every subagent turn, in a directory on your disk. A dozen lines of Python turns that into a table.
- **The steering levers already exist.** Two environment variables and a frontmatter field will pin subagents to a cheap model and a low effort level. No proxy, no runtime, no vendor.

## The thing everybody repeats

I'm not the only one carrying this around. Ask in most Claude Code communities and you'll get the same answer: Explore runs on Haiku, it's the cheap one. It's the reason people wave off the cost of a search-heavy session.

Claude Code ships as a single compiled binary, and the built-in agent definitions are sitting inside it as plain JavaScript objects. So I went and looked.

```python
import re, os
B = os.path.expanduser("~/.local/share/claude/versions/2.1.266")
d = open(B, 'rb').read().decode('utf-8', 'replace')
for m in re.finditer(r'agentType:"([\w-]+)"', d):
    seg = d[m.start():m.start() + 900]
    mo = re.search(r'model:("?[\w-]+"?|\{[^}]*\})', seg)
    print(m.group(1), '->', mo.group(1) if mo else 'none')
```

Explore's definition says `model: "inherit"`. There's a resolver next to it that caps it at Opus on a first-party account, and an environment variable that removes even that cap. Nowhere does it say Haiku.

The only built-ins pinned to a small model are `claude-code-guide`, which answers questions about Claude Code itself, and `statusline-setup`. Everything else (Explore, Plan, general-purpose) inherits your session model.

## Then I checked what actually ran

The code says what's supposed to happen. I wanted the billing record.

It turns out Claude Code already logs this. Every session writes a JSONL transcript under `~/.claude/projects/<your-project>/`, and every subagent gets its own file in a `subagents/` subdirectory next to it. Each assistant line carries the model, and (this is the part I didn't know) the effort level too.

I had 800 of those files sitting on my laptop. Joining each one back to the agent type that spawned it took about a dozen lines:

```python
import json, glob, collections, os

def tool_result_id(msg):
    for b in (msg.get('content') or []) if isinstance(msg, dict) else []:
        if isinstance(b, dict) and b.get('type') == 'tool_result':
            return b.get('tool_use_id')

sub, agent2type = {}, {}
for f in glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')):
    for line in open(f):
        try: d = json.loads(line)
        except: continue
        msg = d.get('message')
        if isinstance(msg, dict):
            for b in msg.get('content') or []:
                if isinstance(b, dict) and b.get('type') == 'tool_use' and b.get('name') in ('Task', 'Agent'):
                    st = (b.get('input') or {}).get('subagent_type')
                    if st: sub[b['id']] = st
        tr = d.get('toolUseResult')
        if isinstance(tr, dict) and tr.get('agentId'):
            tid = d.get('toolUseID') or tool_result_id(msg)
            if tid in sub: agent2type[tr['agentId']] = sub[tid]

res = collections.defaultdict(collections.Counter)
for f in glob.glob(os.path.expanduser('~/.claude/projects/*/*/subagents/agent-*.jsonl')):
    t = agent2type.get(os.path.basename(f)[6:-6])
    if not t: continue
    for line in open(f):
        try: d = json.loads(line)
        except: continue
        msg = d.get('message')
        if isinstance(msg, dict) and msg.get('model'):
            res[t][(msg['model'], d.get('effort'))] += 1

for k in sorted(res):
    print(k, dict(res[k]))
```

Here's what came back for Explore, counted in assistant turns:

| Model | Turns |
| --- | --- |
| `claude-opus-4-8` | 3,932 |
| `claude-opus-5` | 401 |
| `claude-opus-4-6` | 371 |
| `claude-sonnet-5` | 91 |
| `claude-haiku-4-5` | **0** |

Four different models across months of work. Not one Haiku turn. The effort column tracked my session too: Explore ran at `high`, `medium`, and `xhigh` depending on where I'd left the slider.

Every "cheap" file search I've run this year was billed at Opus rates, at whatever effort I'd set for the hard problem I was actually working on. I've [looked at that meter before](/posts/replacing-employees-with-ai-token-cost/). This is one of the places the money goes.

## The levers were already there

Here's where I'd do something different from Portal, starting from the same problem.

Claude Code has model and effort steering built in. It's undocumented, which is a real complaint, but it doesn't need a runtime.

`CLAUDE_CODE_SUBAGENT_MODEL` sets the default model for any subagent whose caller didn't name one explicitly. Set it to `haiku` and Explore falls to Haiku. There's a companion flag, `CLAUDE_CODE_SUBAGENT_MODEL_FORCE`, that makes it absolute. It strips the `model` parameter out of the agent-spawning tool's schema entirely, so the model can't override you even if it wants to.

That last part matters more than it sounds. Claude Code's own system prompt currently tells the model to *omit* the model parameter so subagents inherit the session model, and explicitly not to downshift work to a weaker model on its own judgment. The default posture is expensive, and `CLAUDE.md` won't change it. The harness reads config, not requests.

I hit the mirror image of this when I was [getting Claude Code to lead with the answer](/posts/steering-claude-code-bluf/). There, no setting existed and prose was the only lever I had. Here the setting exists and prose is the part that does nothing.

Custom agents are the finer-grained version, and they sit alongside [the rest of my global Claude Code config](/posts/claude-code-global-configuration/). A file in `~/.claude/agents/` gets its model honored verbatim, and it supports an effort field I hadn't seen documented anywhere:

```yaml
---
name: scout
description: Read-only code search. Find files, grep symbols, answer "where is X defined".
tools: Read, Grep, Glob, Bash
model: haiku
effort: low
---
```

You still don't pick the agent. Claude does, matching your `description` against the task, so a `description` written for searching is how `scout` gets chosen. Effort resolves per model, so you can pin Haiku to `low` in settings while your main loop stays on Opus at `high`.

## What I'd tell the Spotify author

Most of what these agents do is I/O, and we're all paying thinking prices for it.

But there's a step before building the layer, and it's the step nobody takes: find out what you're already running. The data is on your disk. Nobody has to ship you anything.

I'd guess a meaningful chunk of that 90% bulk-read saving is available from two environment variables and an agent file. Not all of it. Routing to Gemini Flash genuinely leaves the Anthropic price list, and no amount of config does that. If your bulk reads are enormous and constant, a cheaper external model is a real answer. I have no reason to doubt their numbers.

The uncomfortable part isn't that I was wrong about Haiku. It's that I was wrong in the expensive direction, carried it around for months, and never spent the ten minutes it would have taken to check. I found the same thing when I [audited the rest of my config](/posts/deleting-my-claude-code-config/): six rules quietly contradicting each other.

One caveat: everything above comes from reading a compiled binary, version 2.1.266. These are internals, not a documented API. The agent roster and the models moved between the versions I have on disk, and they'll move again. Which is the argument for the measurement script rather than the table. Re-run it after every upgrade and trust what your own transcripts say, including over this post.

Check your own numbers before you build anything. Mine were four models deep and none of them were the one I expected.

