Explicit `--model` with deprecated Opus IDs is silently remapped to the current default Opus
Summary
When an explicitly pinned model ID is one of the deprecated Opus 4.0/4.1 identifiers, Claude Code silently substitutes the current default Opus model (claude-opus-4-8). The request succeeds, the substituted model generates all output, and nothing in the CLI output, result JSON, or logs indicates that the requested model was not used. The only opt-out is an undocumented environment variable.
Environment
- Claude Code 2.1.170 (current latest at time of writing) — also reproduced on 2.1.162
- macOS (Darwin 25.3.0), arm64, native installer binary
Reproduction
claude -p --model claude-opus-4-0 --output-format json "Reply with just the word: hello"
Result: is_error: false, and modelUsage contains only claude-opus-4-8. The same happens for:
claude-opus-4-0claude-opus-4-20250514claude-opus-4-1claude-opus-4-1-20250805
The remap is specific to these four IDs (they appear as a literal list in the bundle). For comparison:
--model claude-sonnet-4-5→ honored, served asclaude-sonnet-4-5--model claude-opus-4-5/claude-opus-4-6→ honored--model claude-sonnet-4-0/claude-sonnet-4-20250514→ honored, despite being deprecated with the same June 15, 2026 retirement date as the remapped Opus IDs — so the remap isn't even consistent across the models it's ostensibly protecting--model claude-banana-9000→ fails loudly with a 404, as expected
So the model string is otherwise passed through and validated; these four are intercepted client-side before the request is made.
The substitution is surfaced interactively, but not programmatically
Interactive mode does show a notice — assembled from a deprecation table in the bundle (modelName / retirementDate / remappedTo: "the latest Opus"), rendered along the lines of "Claude Opus 4 now runs as the latest Opus — /model to change." Credit where due. But the notice offers no way to keep the model you pinned: the /model picker doesn't list the deprecated IDs, and the env var that actually preserves the pin (below) is mentioned nowhere.
Programmatic surfaces are silent. Verified on 2.1.170:
- Result JSON (
-p --output-format json): no warning, no remap field, and the requested ID appears nowhere in the output.modelUsagereports the served model (claude-opus-4-8) as if it were simply the session's model. - Stream JSON (
--output-format stream-json --verbose): thesystem/initevent reportsmodel: claude-opus-4-8from the first event, with no indication it differs from what was requested. - stderr /
--debug: nothing remap-related is logged.
This split is backwards for the populations at risk: an interactive user who typed --model claude-opus-4-0 is told; an agent harness, CI pipeline, or eval suite — the callers for whom an exact pin is most load-bearing — gets no signal at all. The served model is reported in modelUsage, so a caller who already knows to compare it against the requested ID can detect the swap after the fact (that is how we discovered this), but the requested ID is never echoed back, so there is nothing to diff against within the output itself.
Workaround (undocumented)
CLAUDE_CODE_DISABLE_LEGACY_MODEL_REMAP=1 claude -p --model claude-opus-4-0 ...
With this set, modelUsage correctly shows claude-opus-4-0. Verified on both 2.1.162 and 2.1.170. This variable does not appear in the documentation; we found it by inspecting strings in the binary.
Why this matters
For interactive chat use, transparently upgrading a soon-to-retire model is arguably user-friendly. But --model with a full, exact model ID is an explicit pin, and Claude Code is increasingly used as a backend by agent harnesses where the specific model is load-bearing:
- Behavioral integrity — long-running agents with persistent identity/memory had every response generated by a different model than configured, with no signal. We discovered this only because the agent's writing voice noticeably changed.
- Eval/research integrity — anyone benchmarking or comparing pinned models is silently measuring the wrong model.
- Auditability — the substitution is only discoverable after the fact by inspecting
modelUsageor transcript message metadata, which most callers never read.
Expected behavior (any of these would be fine)
- Fail loudly when an explicitly pinned model is intercepted, the way an unknown model already does; or
- Warn visibly — stderr notice in interactive mode, and a dedicated field in the
-presult JSON (e.g.model_remapped: {requested, served}); or - Make the remap opt-in, or at minimum document both the remap behavior and
CLAUDE_CODE_DISABLE_LEGACY_MODEL_REMAP.
The motivation for the remap is understandable (these models retire June 15, 2026, and a hard error would strand users with stale settings). The issue is not that a migration path exists — it's that an explicit pin is overridden silently, and the escape hatch is unfindable without reverse-engineering the binary.
---
🤖 Investigated and written with Claude Code