[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/modeloverride) - 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.modelvalue 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
opusplanexists (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
/modeloverride, since the underlying file was read and confirmed correct at the time of at least one occurrence.
Ask
- Confirm whether
opusplanis intended to guarantee Opus for every plan-mode turn, or whether silent fallback-to-Sonnet is expected under some condition (rate limiting, availability). - 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.
- Separately investigate whether
settings.json.modelcan be silently overwritten by a/modelsession override — if confirmed, that is an independent config-integrity bug.
6 Comments
This was working for me last week, but is now longer working this week.
Downgrading
claude install 2.1.200seems to resolve the issue for me.https://code.claude.com/docs/en/changelog#2-1-201
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). NofallbackModelconfigured 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):
claude-opus-4-8, as expected.claude-opus-4-8andclaude-sonnet-518 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.claude-sonnet-5exclusively - including the turn that wrote the final plan file and the turn that calledExitPlanMode. 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_tokensdrops to ~0 andcache_creation_input_tokensspikes 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.
Ive installed the latest version this morning '2.1.202' and this seems to be resolved for me now... 🤷♂️
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.modelhad 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.I used this prompt to test.
/model opusplan
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.
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:assistantrecord carriesmessage.model— the model that actually served that turnuserrecord carriespermissionMode, and"plan"does appear thereSo 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 thepermissionModefield on the user record instead.Two limits on what a transcript can settle, both relevant here:
ANTHROPIC_DEFAULT_OPUS_MODELand friends — is not recorded as structured data. A transcript can show which model answered; it cannot show which one was intended.settings.jsonedit 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/routeledger —
npx routeledgerprints the per-turn model timeline and separates model changes preceded by a/modelcommand from those that were not. Where the fields needed are absent it reportsunverifiablerather 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.