[BUG] opusplan/haiku in plan mode: subagents (e.g. Explore) inherit plan-upgrade model instead of their defined model

Status Fixed / completed
Reported on v2.1.175
Maintainer reply None cached
Activity 4 comments · opened Jun 12, 2026 · closed Jul 2, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

With the opusplan model alias in plan mode, every spawned subagent — including the built-in Explore agent whose definition is model: "haiku" — runs on claude-opus-4-8 instead of claude-haiku-4-5-20251001. Per-invocation model picks on the Agent tool are silently overridden the same way.

The same mechanism affects the haiku alias: in plan mode its subagents run on sonnet.

Reproduction matrix (CC 2.1.175, verified on both interactive TUI and headless claude -p):

| alias | permissionMode | parent model | Explore subagent model | OK? |
|---|---|---|---|---|
| opus | default | opus-4-8 | haiku-4-5 | OK |
| opus[1m] | default / plan | opus-4-8 | haiku-4-5 | OK |
| sonnet / sonnet[1m] | default / plan | sonnet-4-6 | haiku-4-5 | OK |
| opusplan | default | sonnet-4-6 | haiku-4-5 | OK |
| opusplan | plan | opus-4-8 | opus-4-8 | BUG |
| opusplan[1m] | plan | opus-4-8 | opus-4-8 | BUG |
| haiku | default | haiku-4-5 | haiku-4-5 | OK |
| haiku | plan | sonnet-4-6 | sonnet-4-6 | BUG |

The discriminator is the alias alone: same binary, same session, only the two mode-dependent aliases (opusplan, haiku) break. Counterintuitively, opusplan — the alias designed to save cost — is more expensive in plan mode than plain opus, which routes Explore to haiku correctly (~5x cost difference: Haiku 4.5 $1/$5 vs Opus 4.8 $5/$25 per MTok).

What Should Happen?

The Explore subagent should run on claude-haiku-4-5-20251001 as set by its built-in definition (model: "haiku"), regardless of which alias the parent session uses. Per-invocation model parameters on the Agent tool should also be honored. The opusplan plan-mode upgrade should apply only to the main conversation loop, not to subagent queries.

Error Messages/Logs

# claude --debug-to-stderr --model opusplan --permission-mode plan -p "Use the Explore agent to list files..."
# All API requests dispatch on opus, including the Explore subagent's:
2026-06-12T14:09:19.220Z [DEBUG] [API REQUEST] /v1/messages x-client-request-id=f31a57ab-... source=agent:builtin:Explore
2026-06-12T14:09:23.992Z [DEBUG] [API REQUEST] /v1/messages x-client-request-id=581700f8-... source=agent:builtin:Explore
# grep "dispatching to firstParty model=" -> 4x "claude-opus-4-8[1m]", zero "haiku" occurrences in the entire log.
# Notably ABSENT: the warning "Subagent model \"haiku\" is not in the availableModels allowlist; inheriting the parent model instead"
# -> the subagent model resolver succeeded; the override happens downstream (see Additional Information).

Steps to Reproduce

  1. Run (correct behavior, for contrast):

``bash
claude --model "opus[1m]" --permission-mode plan \
-p "Use the Explore agent to list files in the current directory."
`
Check
~/.claude/projects/<proj>/<session-id>/subagents/agent-*.jsonl -> .message.model = claude-haiku-4-5-20251001` (correct)

  1. Run (bug):

``bash
claude --model opusplan --permission-mode plan \
-p "Use the Explore agent to list files in the current directory."
`
Same check ->
.message.model = claude-opus-4-8` (wrong)

  1. Same result in interactive mode: /model opusplan + plan mode + any prompt that spawns the Explore agent.
  1. Variant: --model haiku --permission-mode plan -> Explore runs on claude-sonnet-4-6 (wrong)

Claude Model

Other

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.175

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

iTerm2

Additional Information

Root cause (from reading the bundled JS in the 2.1.175 binary):

The runtime model getter (getRuntimeMainLoopModel, minified as mR) re-reads the session-global alias setting (getUserSpecifiedModelSetting(), minified Xl) on every call and applies the plan-mode upgrade unconditionally whenever the session alias is opusplan/haiku and the permission mode is plan:

// actual minified code, CC 2.1.175, reformatted:
function mR(H) {
  let {permissionMode: _, mainLoopModel: q, exceeds200kTokens: K = !1} = H,
      O = Xl();                                     // <- GLOBAL session alias, e.g. "opusplan"
  if ((O === "opusplan" || O === "opusplan[1m]") && _ === "plan" && !K) {
    let $ = O === "opusplan[1m]" || PM() ? qF(MJ()) : MJ();   // opus(-[1m])
    if (!(KI($) ?? o4($))) { /* allowlist warn */ return M9(O); }
    return $;                                       // <- opus, regardless of `mainLoopModel` arg
  }
  if (Xl() === "haiku" && _ === "plan") {
    let z = wG();                                   // sonnet
    if (!(KI(z) ?? o4(z))) { /* allowlist warn */ return M9("haiku"); }
    return z;                                       // <- sonnet, regardless of `mainLoopModel` arg
  }
  return q;                                         // other aliases: pass through
}

The subagent model resolver (Le) works correctly: for Explore it resolves the definition model:"haiku" to the haiku model ID (precedence env > per-invocation > definition > inherit), and that becomes the subagent's options.mainLoopModel.

But the query loop — shared by parent and subagent sessions — recomputes the effective API model through mR at query-setup time:

// between a3("query_setup_start") ... a3("query_setup_end"):
IH = I8(x).mode,    // permission mode, inherited from parent context: "plan"
dH = mR({permissionMode: IH, mainLoopModel: S ?? v[C] ?? E, exceeds200kTokens: uH}),

For a subagent this is mR({permissionMode: "plan", mainLoopModel: <haiku>}) — but Xl() inside still returns "opusplan" (it is session-global), so the opusplan branch fires and returns opus. The correctly-resolved haiku model is discarded. The [1m] suffix observed in dispatch logs is itself a fingerprint of this branch (PM() ? qF(MJ()) : MJ()) — nothing in the Le path appends [1m] to a haiku resolution.

opus/opus[1m]/sonnet have no branch in mR, fall through to return q, and subagents keep their intended model — matching the observed matrix exactly.

Suggested fix (any of):

  1. In the query loop, skip the Xl()-based upgrade branches when the context is a subagent (the resolved mainLoopModel already encodes the right answer), or
  2. Have mR trust its mainLoopModel argument whenever it differs from the session's resting model (i.e., it was already explicitly resolved), or
  3. Spawn subagents with their own permission mode rather than inheriting "plan".

Workaround: use opus[1m] (or plain opus) instead of opusplan.

Related: #29768 (closed as not planned) — same symptom reported earlier without root cause isolation. This report narrows it to the mode-dependent aliases (opusplan, haiku) and pins the exact mechanism; plain aliases are unaffected.

View original on GitHub ↗

4 Comments

liqlvnvn · 3 months ago

Re-tested on CC 2.1.185 (2026-06-22) — still reproduces

Ran the full behavioural matrix headless (claude -p, scratchpad dir), extracting .message.model from subagent transcripts (subagents/agent-*.jsonl):

| alias | permissionMode | Explore .message.model | expected | result |
|---|---|---|---|---|
| opusplan | plan | claude-opus-4-8 | claude-haiku-4-5-20251001 | ✗ |
| haiku | plan | claude-sonnet-4-6 | claude-haiku-4-5-20251001 | ✗ |
| opus[1m] | plan | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✓ |
| sonnet | plan | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✓ |
| opusplan | default | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✓ |

Binary confirmation (2.1.185): The mR function has been renamed to c0 and Xl() to j7(), but the logic is structurally identical — the plan-mode upgrade branches still read the session-global alias instead of the passed mainLoopModel. Reconstructed from binary:

function c0({permissionMode: t, mainLoopModel: n, exceeds200kTokens: r = false}) {
  let o = j7();  // global alias — same bug: ignores `n`
  if ((o === "opusplan" || o === "opusplan[1m]") && t === "plan" && !r) {
    return xy();  // ← opus-4-8, regardless of what mainLoopModel was
  }
  if (j7() === "haiku" && t === "plan") {
    return mk();  // ← sonnet-4-6, regardless of what mainLoopModel was
  }
  return n;       // correct fallthrough (aliases without a plan branch)
}

Workarounds (still valid): Use opus[1m] instead of opusplan — no plan-mode branch in c0, subagents get their intended model. Or set CLAUDE_CODE_SUBAGENT_MODEL=haiku before launching.

liqlvnvn · 3 months ago

Re-test on CC 2.1.191 — full 32-cell matrix

Ran a full headless sweep on 2.1.191 (macOS, Darwin 25.5.0, Anthropic API): 8 aliases × 4 permission modes = 32 cells, measuring the Explore subagent's .message.model from the session JSONL ($PROJ/$sid/subagents/agent-*.jsonl).

Verdict: #67942 still reproduces on 2.1.191. 29 OK / 3 BUG / 0 inconclusive.

Full matrix

| alias | mode | parent model | Explore model | expected | verdict |
|---|---|---|---|---|---|
| opus | default | claude-opus-4-8 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| opus | plan | claude-opus-4-8 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| opus | acceptEdits | claude-opus-4-8 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| opus | bypassPermissions | claude-opus-4-8 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| opus[1m] | default | claude-opus-4-8 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| opus[1m] | plan | claude-opus-4-8 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| opus[1m] | acceptEdits | claude-opus-4-8 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| opus[1m] | bypassPermissions | claude-opus-4-8 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| sonnet | default | claude-sonnet-4-6 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| sonnet | plan | claude-sonnet-4-6 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| sonnet | acceptEdits | claude-sonnet-4-6 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| sonnet | bypassPermissions | claude-sonnet-4-6 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| sonnet[1m] | default | claude-sonnet-4-6 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| sonnet[1m] | plan | claude-sonnet-4-6 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| sonnet[1m] | acceptEdits | claude-sonnet-4-6 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| sonnet[1m] | bypassPermissions | claude-sonnet-4-6 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| haiku | default | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| haiku | plan | claude-sonnet-4-6 | claude-sonnet-4-6 | claude-haiku-4-5-20251001 | ❌ BUG |
| haiku | acceptEdits | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| haiku | bypassPermissions | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| opusplan | default | claude-sonnet-4-6 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| opusplan | plan | claude-opus-4-8 | claude-opus-4-8 | claude-haiku-4-5-20251001 | ❌ BUG |
| opusplan | acceptEdits | claude-sonnet-4-6 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| opusplan | bypassPermissions | claude-sonnet-4-6 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| opusplan[1m] | default | claude-sonnet-4-6 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| opusplan[1m] | plan | claude-opus-4-8 | claude-opus-4-8 ¹ | claude-haiku-4-5-20251001 | ❌ BUG |
| opusplan[1m] | acceptEdits | claude-sonnet-4-6 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| opusplan[1m] | bypassPermissions | claude-sonnet-4-6 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| default | default | claude-opus-4-8 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| default | plan | claude-opus-4-8 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| default | acceptEdits | claude-opus-4-8 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |
| default | bypassPermissions | claude-opus-4-8 | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ OK |

¹ Secondary symptom for opusplan[1m] × plan: the [1m] extended-thinking flag is silently dropped on the subagent — parent runs claude-opus-4-8 with extended thinking, Explore subagent gets plain claude-opus-4-8.

Key observations

  • All 3 bugs are permissionMode=plan only — no regressions outside the predicted set.
  • opus[1m] × plan is not affected — the bug is gated on the plan-upgrade branch inside opusplan/haiku aliases, not plain aliases with [1m].
  • opusplan × default correctly resolves to claude-sonnet-4-6 (lighter model, no bug). The upgrade only misfires in plan mode.
  • default × plan is unaffected — no plan-upgrade branch in the default alias.
  • Cost impact: opusplan/opusplan[1m] × plan → ~15× cost overrun per Explore subagent call; haiku × plan → ~3×.

Root cause (unchanged from original report): mR reads the session-global alias at subagent query-setup and fires the plan-mode upgrade branch unconditionally, discarding the correctly-resolved per-subagent mainLoopModel. Plain aliases hit return q → unaffected.

liqlvnvn · 3 months ago

Thanks for fixing the underlying inconsistency in 2.1.198. Closing the loop with a full re-test below.

Re-tested on CC 2.1.198 (2026-07-02) — 32 of 32 cells consistent, 0 BUG cells.

The 2.1.198 changelog entry:

The built-in Explore agent now inherits the main session's model (capped at opus) instead of running on haiku

removes the haiku-default premise this bug depended on. Since Explore no longer has a
"default to haiku unless overridden" behavior at all — it now always inherits the
parent's resolved model (capped at opus) — the three previously-broken cells
(opusplan+plan, opusplan[1m]+plan, haiku+plan) are no longer special-cased:
they just inherit like every other cell.

| alias | mode | parent model | Explore model | status |
|---|---|---|---|---|
| opus | default | claude-opus-4-8 | claude-opus-4-8 | ✅ ok |
| opus | plan | claude-opus-4-8 | claude-opus-4-8 | ✅ ok |
| opus | acceptEdits | claude-opus-4-8 | claude-opus-4-8 | ✅ ok |
| opus | bypassPermissions | claude-opus-4-8 | claude-opus-4-8 | ✅ ok |
| opus[1m] | default | claude-opus-4-8 | claude-opus-4-8 | ✅ ok (explicit id, alias broken*) |
| opus[1m] | plan | claude-opus-4-8 | claude-opus-4-8 | ✅ ok (explicit id, alias broken*) |
| opus[1m] | acceptEdits | claude-opus-4-8 | claude-opus-4-8 | ✅ ok (explicit id, alias broken*) |
| opus[1m] | bypassPermissions | claude-opus-4-8 | claude-opus-4-8 | ✅ ok (explicit id, alias broken*) |
| sonnet | default | claude-sonnet-5 | claude-sonnet-5 | ✅ ok |
| sonnet | plan | claude-sonnet-5 | claude-sonnet-5 | ✅ ok |
| sonnet | acceptEdits | claude-sonnet-5 | claude-sonnet-5 | ✅ ok |
| sonnet | bypassPermissions | claude-sonnet-5 | claude-sonnet-5 | ✅ ok |
| sonnet[1m] | default | claude-sonnet-5 | claude-sonnet-5 | ✅ ok (explicit id, alias broken*) |
| sonnet[1m] | plan | claude-sonnet-5 | claude-sonnet-5 | ✅ ok (explicit id, alias broken*) |
| sonnet[1m] | acceptEdits | claude-sonnet-5 | claude-sonnet-5 | ✅ ok (explicit id, alias broken*) |
| sonnet[1m] | bypassPermissions | claude-sonnet-5 | claude-sonnet-5 | ✅ ok (explicit id, alias broken*) |
| haiku | default | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ ok |
| haiku | plan | claude-sonnet-5 | claude-sonnet-5 | ✅ ok |
| haiku | acceptEdits | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ ok |
| haiku | bypassPermissions | claude-haiku-4-5-20251001 | claude-haiku-4-5-20251001 | ✅ ok |
| opusplan | default | claude-sonnet-5 | claude-sonnet-5 | ✅ ok |
| opusplan | plan | claude-opus-4-8 | claude-opus-4-8 | ✅ ok |
| opusplan | acceptEdits | claude-sonnet-5 | claude-sonnet-5 | ✅ ok |
| opusplan | bypassPermissions | claude-sonnet-5 | claude-sonnet-5 | ✅ ok |
| opusplan[1m] | default | claude-opus-4-8 | claude-opus-4-8 | ⚠️ ok, opus[1m] proxy only† |
| opusplan[1m] | plan | claude-opus-4-8 | claude-opus-4-8 | ✅ ok (real alias) |
| opusplan[1m] | acceptEdits | claude-opus-4-8 | claude-opus-4-8 | ⚠️ ok, opus[1m] proxy only† |
| opusplan[1m] | bypassPermissions | claude-opus-4-8 | claude-opus-4-8 | ⚠️ ok, opus[1m] proxy only† |
| default | default | claude-opus-4-8 | claude-opus-4-8 | ✅ ok |
| default | plan | claude-opus-4-8 | claude-opus-4-8 | ✅ ok |
| default | acceptEdits | claude-opus-4-8 | claude-opus-4-8 | ✅ ok |
| default | bypassPermissions | claude-opus-4-8 | claude-opus-4-8 | ✅ ok |

\* Separate finding, unrelated to this bug: --model 'opus[1m]' / 'sonnet[1m]' currently fail outright with "Advisor disabled — base model isn't in the advisor capability table," then "may not exist or you may not have access." The explicit full model ID with the same suffix (claude-opus-4-8[1m], claude-sonnet-5[1m]) works fine, so these 8 rows were re-tested with the explicit ID as a substitute for the alias.

† opusplan[1m] outside plan mode hits the same broken alias lookup and has no 1:1 explicit-ID substitute (it's a routing alias, not a concrete model), so these 3 rows use claude-opus-4-8[1m] as a stand-in to confirm Explore inherits correctly for 1M-context sessions in general — they don't confirm what opusplan[1m] itself resolves to outside plan mode. The one opusplan[1m] cell this bug is actually about (+plan) was tested via the real alias and is solid.

Closing this out on my end. Two side notes for future readers:

  1. This is a behavior change, not a restoration of the old haiku default. Anyone who relied on Explore being cheap-by-default (haiku pricing) on opus/opusplan sessions will now pay parent-model rates for it; CLAUDE_CODE_SUBAGENT_MODEL=haiku still forces it back down if that's wanted, confirmed with a spot-check on this build.
  2. Unrelated to Explore/subagent routing, but found while re-testing: the <alias>[1m] shorthand (opus[1m], sonnet[1m]) is currently broken on 2.1.198 as described above. Happy to file that separately if useful — flagging here since it's what forced the explicit-ID substitution in the table.
github-actions[bot] · 1 month ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.