[Bug] `opusplan` silently falls back to Sonnet during plan mode (no signal)

Status Open
Maintainer reply None cached
Activity 6 comments · opened Jul 5, 2026

Environment

  • Product: Claude Code (CLI)
  • Model configuration: ~/.claude/settings.json"model": "opusplan" (persistent global config, not a session-only /model override)
  • Platform: macOS (Darwin 25.5.0)
  • Observed across multiple sessions over several weeks

Symptom

opusplan is intended so that plan mode runs on Opus and execution mode automatically switches to Sonnet. In practice, plan-mode turns have been observed running on Sonnet even though settings.json was verified (by reading the file, not by assertion) to contain "opusplan" at the time. There is no error, warning, or status-line signal when this happens — the only tell is behavioral (an agent reasoning at Sonnet-level depth on a plan-mode turn), or session inspection after the fact.

Expected behavior

When settings.json .model is "opusplan":

  • Every plan-mode turn executes on Opus.
  • Every execution-mode turn (post plan-approval) executes on Sonnet.
  • If the runtime cannot honor this (availability, rate limit, internal fallback), it should surface a visible signal — not silently downgrade a turn the user believes is running on Opus.

Actual behavior

  • Plan-mode turns have executed on Sonnet despite a verified-correct "opusplan" setting, with no error or status-line indication of the fallback.
  • In one session the settings.json .model value itself was found silently rewritten to "sonnet" outside of an explicit user action — cause unconfirmed, but noted as a related data point since it compounds the trust problem (a user can no longer assume "I set opusplan and it stayed set"). This may be a separate config-integrity bug.

Impact

  • Silent model downgrade during plan mode undermines the entire reason opusplan exists (Opus-quality planning, cost-optimized Sonnet execution).
  • Because there is no error signal, the only detection mechanism is behavioral suspicion + manual file re-verification. A local SessionStart hook can warn on config drift at session start, but cannot detect a silent per-turn fallback.

What we've ruled out

  • Not a one-time typo: verified in multiple independent sessions.
  • Not solely a stale /model override, since the underlying file was read and confirmed correct at the time of at least one occurrence.

Ask

  1. Confirm whether opusplan is intended to guarantee Opus for every plan-mode turn, or whether silent fallback-to-Sonnet is expected under some condition (rate limiting, availability).
  2. If fallback is by design, add a visible signal (status-line marker, transcript annotation) when a plan-mode turn executes on a model other than Opus.
  3. Separately investigate whether settings.json .model can be silently overwritten by a /model session override — if confirmed, that is an independent config-integrity bug.

View original on GitHub ↗

6 Comments

JamieBriersBullhorn · 1 month ago

This was working for me last week, but is now longer working this week.

Downgrading claude install 2.1.200 seems to resolve the issue for me.

https://code.claude.com/docs/en/changelog#2-1-201

netopsengineer · 1 month ago

I have a transcript that reproduces this closely and adds some hard data. Session ran on Claude Code v2.1.201, with ~/.claude/settings.json -> "model": "opusplan" (global, persistent, verified by reading the file directly, not by assumption). No fallbackModel configured anywhere (checked global, project, and local settings) - so the documented "fallback model chains" mechanism wasn't even eligible to trigger, and no fallback notice appears anywhere in the transcript.

Timeline (single continuous Plan Mode session - permission-mode stayed "plan" from entry to exit, never left):

  • Phase 1 (~74 min): every plan-mode turn ran on claude-opus-4-8, as expected.
  • Phase 2 (~38 min): the model began flipping between claude-opus-4-8 and claude-sonnet-5 18 times, sometimes twice within the same minute, still fully inside Plan Mode, with no user message, no /model command, and no permission-mode change coinciding with any flip.
  • Phase 3 (rest of session, ~7 min to plan submission): locked onto claude-sonnet-5 exclusively - including the turn that wrote the final plan file and the turn that called ExitPlanMode. I ended up rejecting that plan; in hindsight the back half of it was produced entirely by Sonnet, not Opus.

Evidence these were real underlying model swaps, not just a mislabeled field: each flip coincides with a prompt-cache reset (cache_read_input_tokens drops to ~0 and cache_creation_input_tokens spikes to match total context), consistent with Opus and Sonnet holding separate caches - i.e., these were genuinely different API calls to different models, not a cosmetic labeling bug.

Ruled out the older 200k-token-guard mechanism (#65512, closed): context did cross ~200k around when flipping started, but Opus continued to be used well after that at ~225k, ~255k, and ~266k tokens (interspersed with Sonnet), so this doesn't look like that specific hardcoded-200k-threshold bug - it behaves more erratically than a clean threshold.

Bonus data point, possibly related to #67942 (closed 2026-07-02, 4 days before this session): a subagent spawned mid-session via the Agent/Task tool - no model override in the spawn call - ran all of its ~27 turns on Sonnet, even though the parent/orchestrator turn that spawned it was on Opus at that exact moment and Plan Mode was active throughout. That's the mirror image of #67942's symptom (there, subagents over-inherited Opus; here, one under-inherited to Sonnet), which makes me wonder if the fix for #67942 is what introduced this regression - I can't confirm that without the source diff, but the timing (fix on 7/2, this open regression report on 7/5, my own repro on 7/6, all on 2.1.201) lines up suspiciously well.

Happy to share the raw JSONL transcript slices (with project-specific content redacted) if that's useful for debugging.

JamieBriersBullhorn · 1 month ago

Ive installed the latest version this morning '2.1.202' and this seems to be resolved for me now... 🤷‍♂️

knowledgecode · 1 month ago

I'm seeing the same symptom. In my environment, during plan mode, once context crosses ~200K it falls back from Opus to Sonnet, and then once auto-compaction kicks in and brings context back under 200K, the next plan-mode turn switches back to Opus on its own, with no action from me — I've confirmed this full cycle repeatedly across multiple sessions (adding it since I didn't see it mentioned in this thread yet).

One more thing: today I happened to notice settings.json's .model had changed from "opusplan" to "sonnet". This matches the concern raised in the issue body, but it's a single occurrence and I haven't been able to reproduce it since, so I can't say whether it's related to the above or not. Just leaving it here as a data point.

  • Product: Claude Code (CLI), version 2.1.207
  • Model configuration: opusplan
  • Plan: Business Premium seat
jadeforrest · 1 month ago

I used this prompt to test.

/model opusplan

I have the model currently set to opusplan. I'd like to do a trivial proof that it is working. Make the simplest possible (low token use) plan, and include in the output of the plan the model that produced the plan (with version), which does something like call the date command. Then once we're ready, we'll execute that plan, and I'd like you to verify which model is being used to execute the plan (with version).

The plan showed that is was generated with sonnet 5 (The UI also reflected that). I believe this might be related to the Opus 5 upgrade, so might be unrelated to the above issues.

OnourImpram · 1 month ago

The transcript already holds the evidence for this per turn, which makes the report provable rather than anecdotal.

In ~/.claude/projects/<slug>/<session-id>.jsonl:

  • every assistant record carries message.model — the model that actually served that turn
  • every user record carries permissionMode, and "plan" does appear there

So plan-mode turns can be attributed to a model directly, and a plan phase that never upgraded shows up as plan-mode turns and execution turns sharing one model id.

One gotcha worth recording for anyone scripting this: there are also standalone type:"permission-mode" records, and they carry no timestamp and no uuid (measured on 2.1.220), so they cannot be ordered against assistant turns reliably. Use the permissionMode field on the user record instead.

Two limits on what a transcript can settle, both relevant here:

  • the environment a session ran with — ANTHROPIC_DEFAULT_OPUS_MODEL and friends — is not recorded as structured data. A transcript can show which model answered; it cannot show which one was intended.
  • a mid-session settings.json edit and a resume are not recorded either, so "no user action caused this" is an inference, not a finding.

I packaged this into a small read-only CLI because I needed it for my own sessions: https://github.com/OnourImpram/routeledgernpx routeledger prints the per-turn model timeline and separates model changes preceded by a /model command from those that were not. Where the fields needed are absent it reports unverifiable rather than "fine".

AI-assistance disclosure: this comment was drafted with Claude Code under my authorization, and every claim it makes about the transcript format was verified against real transcripts before posting.