Chapter 3. Claude Code Models, Reasoning Effort, and the Context Window

Claude Code takes a model as an alias or a full name, and nine aliases exist. Resolution depends on your plan, your provider, and your binary version: default gives Opus 5 on Max and Sonnet 5 on Pro, best selects Fable 5 where the org has access, opusplan runs opus in plan mode and sonnet for execution, and [1m] picks the 1M context window.

The binary on my Mac reports 2.1.211. That number decides which models I can select before any account tier or provider question enters the picture: the docs put Opus 5 selection at v2.1.219 or later, so the newest Opus does not exist for this machine until I update it. The pin is deliberate. claude doctor reports auto-updates disabled by the DISABLE_AUTOUPDATER env var, with the last update attempt succeeding to 2.1.211 on 2026-07-16. A pinned binary is a stable binary, and the price of stability is that documentation and installed behavior drift apart. Run claude --version before you trust any model claim against your own install, and treat every version gate below as part of the fact it gates.

The nine aliases, and what default resolves to

Claude Code exposes nine model aliases: default, best, fable, sonnet, opus, haiku, sonnet[1m], opus[1m], and opusplan. Two of them do more than point at a model.

default is a reset value rather than an alias. It hands selection back to whatever your account type resolves to, and that resolution splits by plan and provider:

Account or provider default resolves to
Max, Team Premium, Enterprise pay-as-you-go Opus 5
Anthropic API, Claude Platform on AWS, Amazon Bedrock, Google Cloud’s Agent Platform Opus 5
Pro, Team Standard, Enterprise subscription seats Sonnet 5
Microsoft Foundry Sonnet 4.5

best selects Fable 5 where the org has access and otherwise the latest Opus. opusplan is a compound: it uses opus during plan mode and switches to sonnet for execution, which buys the expensive model’s judgment for the decisions and the cheaper model’s throughput for the typing. opusplan[1m] forces the 1M context window for both phases.

On 2.1.211, claude --model --help prose names fable, opus, and sonnet as aliases and gives claude-fable-5 as the example of a full model name. Full names work anywhere an alias does.

Why opus and sonnet mean different models on different providers

An alias is a request, and the provider interprets it. The same word lands on different models depending on where your traffic goes:

Provider opus sonnet
Anthropic API Opus 5 Sonnet 5
Claude Platform on AWS Opus 5 Sonnet 4.6
Amazon Bedrock Opus 5 Sonnet 4.5
Google Cloud’s Agent Platform Opus 5 Sonnet 4.5
Microsoft Foundry Opus 4.6 Sonnet 4.5

Layer the binary version on top of that. Minimum Claude Code versions by model: Opus 5 requires v2.1.219 or later, Sonnet 5 requires v2.1.197, Opus 4.8 requires v2.1.154, Fable 5 requires v2.1.170. A script that says --model opus means one thing on my laptop and a different thing in a Bedrock-backed CI job, and neither of those is a bug. Write the full model name into anything that has to behave the same in two places.

Four ways to set the model, and which one wins

Four mechanisms select the model, and their precedence is fixed: /model in the session wins, then --model at startup, then the ANTHROPIC_MODEL env var, then the model field in settings.

# startup flag
claude --model fable

# environment
ANTHROPIC_MODEL=claude-fable-5 claude

# in session, highest precedence
/model opus[1m]

The in-session command reaches beyond the interactive terminal. In -p (print) mode, /model, /effort, /fast, /color, and /rename accept their value as an argument, as in /model sonnet, from v2.1.205, and /config key=value works from v2.1.181; terminal-only commands such as /login stay unavailable there. The same argument form for /model and /effort works from mobile and web Remote Control. The background-agent surface carries the flag too: on 2.1.211, claude agents --help lists --model <model> alongside --effort <level>.

A second family of env vars remaps what the aliases mean rather than picking a model: ANTHROPIC_DEFAULT_FABLE_MODEL, ANTHROPIC_DEFAULT_OPUS_MODEL, ANTHROPIC_DEFAULT_SONNET_MODEL, and ANTHROPIC_DEFAULT_HAIKU_MODEL. The old ANTHROPIC_SMALL_FAST_MODEL is deprecated in favor of the haiku variable. CLAUDE_CODE_SUBAGENT_MODEL pins the model for subagents; it accepts an alias or a full name, overrides both per-invocation model arguments and subagent frontmatter, and setting it to inherit restores normal resolution. That override strength cuts both ways: a forgotten export in a shared shell profile flattens the per-subagent models a repo chose on purpose.

For gateways and pinned deployments there are picker-shaping variables: ANTHROPIC_CUSTOM_MODEL_OPTION adds one custom entry to the /model picker, with ANTHROPIC_CUSTOM_MODEL_OPTION_NAME and ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION optional (the description defaults to Custom model (<model-id>)). CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 populates the picker from a gateway. When a pinned model needs its capabilities declared, ANTHROPIC_DEFAULT_<FAMILY>_MODEL_SUPPORTED_CAPABILITIES takes a comma-separated list drawn from effort, xhigh_effort (v2.1.111 or later), max_effort, thinking, adaptive_thinking, and interleaved_thinking.

Governance lives in settings keys: availableModels is an allowlist matching a family, a version prefix, or a full ID; enforceAvailableModels extends that allowlist to the Default option and requires v2.1.175; modelOverrides maps Anthropic model IDs to provider-specific strings; model and fallbackModel round out the set.

Prompt caching is switchable per model family through the same env-var pattern. DISABLE_PROMPT_CACHING turns it off for all models and takes precedence; DISABLE_PROMPT_CACHING_HAIKU, DISABLE_PROMPT_CACHING_SONNET, DISABLE_PROMPT_CACHING_OPUS, and DISABLE_PROMPT_CACHING_FABLE, each set to 1, scope the switch to one family. I have no reason to run with caching off in normal work; the per-family variables exist for the case where one family’s caching behavior needs isolating, and knowing the exact names beats rediscovering them mid-incident.

Fallback chains: --fallback-model and the classifier reroute

--fallback-model takes a comma-separated list, as in claude --fallback-model sonnet,haiku, and the persistent equivalent is the fallbackModel settings key as an array. Chains are capped at three models after duplicate removal, the literal string "default" expands to the default model, and a fallback switch lasts for the current turn only. The 2.1.211 help text adds that the primary is retried at the start of each user turn and that the flag works only with --print. That last claim is documentation, not enforcement: I ran claude --fallback-model sonnet hi without --print on this machine and it fell through to normal startup instead of erroring. The stream-json flag family hard-errors on bad combinations; the print-only trio (--fallback-model, --max-budget-usd, --no-session-persistence) does not.

There is also a fallback you do not choose. From v2.1.219, a content classifier can reroute a request by category: on Fable 5, biology-flagged requests re-run on Opus 5 and cybersecurity-flagged requests re-run on Opus 4.8; on Opus 5, cybersecurity-flagged requests re-run on Opus 4.8 while biology-flagged requests end in refusal. In non-interactive mode a flagged request ends the turn with a refusal instead of prompting. If you run headless pipelines, that is a terminal state your error handling has to expect, not a dialog someone will click through.

Which effort levels each model supports

Effort is a per-model capability, not a universal dial:

Model Supported effort levels
Fable 5, Opus 5, Sonnet 5, Opus 4.8, Opus 4.7 low, medium, high, xhigh, max
Opus 4.6, Sonnet 4.6 low, medium, high, max
Models not listed no effort support

The default is high on each model that supports effort, with one exception: Opus 4.7 defaults to xhigh. An unsupported level falls back to the highest supported level at or below it, so xhigh runs as high on Opus 4.6 without complaint. That fallback is convenient in a terminal and a trap in a fleet: the level you wrote and the level you got can differ by model, and nothing fails.

Six ways to set effort, and which one wins

Six mechanisms set effort: /effort <level> in session, /effort auto, the --effort flag at startup, the CLAUDE_CODE_EFFORT_LEVEL env var, the effortLevel settings key, and effort frontmatter in a skill or subagent. The env var takes precedence over all other methods, which inverts the usual expectation that the flag on the command line is the last word. A CLAUDE_CODE_EFFORT_LEVEL exported in a CI environment beats the --effort you pass in the job’s own command. When an automation run behaves as if it ignored your flag, check the environment before you check the flag.

In -p mode, /effort accepts its value as an argument from v2.1.205, same as /model, and the same form works from mobile and web Remote Control.

Validation on the flag is soft, and I confirmed the shape of it on 2.1.211: claude --effort nonsense -p hi does not exit with an error. It prints a warning naming the unknown value, says it is ignoring it and using the default effort, and lists the valid values as low, medium, high, xhigh, max. Compare --output-format, which rejects a bad value with a hard error and an allowed-choices list. The soft path means a typo in a cron job’s effort flag runs the whole job at default effort behind a warning line nobody reads. Grep your automation logs for Unknown --effort value once; it costs nothing.

max, ultracode, and ultrathink

max and ultracode are session-only. Neither is accepted in the effortLevel settings key or in CLAUDE_CODE_EFFORT_LEVEL; the four persistent levels are low, medium, high, and xhigh. You cannot make maximum effort your standing default, and I read that as intentional friction.

ultracode is a Claude Code setting, not a model effort level. It sends xhigh to the model and layers dynamic workflow orchestration on top, on the harness side. Three switches enable it: /effort ultracode in session, claude --effort ultracode at startup, or "ultracode": true inside --settings JSON. The flag form requires v2.1.203; earlier binaries treated ultracode as an unknown effort value and warned.

ultrathink is a different mechanism again. The literal keyword anywhere in a prompt adds an in-context instruction requesting deeper reasoning for that turn; the effort level sent to the API does not change. Phrases like “think”, “think hard”, and “think more” are not recognized keywords and pass through as ordinary prompt text. One magic word, no synonyms.

How I pick an effort level

I pick effort by the consequence of being wrong, not by how interesting the problem is. A rename with a test suite behind it can run at low or medium: a wrong answer trips the suite and costs one re-run. high is the shipped default and the right call for most feature work. A schema migration, an auth change, anything whose last step is a production deploy gets xhigh or max, because the failure mode there is not a red test, it is an incident with my name on it. The pattern I catch myself in, and correct, is the inverted one: burning max on a question I find interesting and letting an irreversible change ride at the default. Curiosity reads the same at any effort level. Blast radius is what the dial is for.

The same stance picks models. opusplan encodes it at the alias level: the plan, where a wrong call compounds through everything built on it, gets opus; the execution, where each step is checkable against the plan, gets sonnet. Spend where the decision is, not where the typing is.

Extended thinking and its own controls

Extended thinking has its own controls, independent of effort. Option+T on macOS (Alt+T on Windows and Linux) toggles thinking for the session. /config sets the global default, persisted as alwaysThinkingEnabled in ~/.claude/settings.json. MAX_THINKING_TOKENS=0 disables thinking on the Anthropic API, except on Fable 5, where it does not. showThinkingSummaries: true surfaces full summaries, and Ctrl+O toggles the verbose display. CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1 reverts to fixed thinking budgets and applies only to Opus 4.6 and Sonnet 4.6.

Two more controls exist on 2.1.211 that claude --help does not list. The parser accepts --thinking <mode> with the choice-validated values enabled, adaptive, and disabled, and --max-thinking-tokens <tokens> with a numeric argument. I found both by probing: unknown options error with unknown option, so a flag the parser accepts exists whether or not the help text admits it.

The 1M context window and which models have it

The 1M-token context window is supported by Fable 5, Sonnet 5, Opus 4.6 and later, and Sonnet 4.6. You select it with the [1m] suffix on an alias or a full model name: /model opus[1m], or claude-opus-4-8[1m] spelled out. CLAUDE_CODE_DISABLE_1M_CONTEXT=1 removes the 1M variants from the picker, which is the lever if long-context cost is a budget problem on a team.

Sonnet 5 on the Anthropic API is the special case: it runs with the 1M window as its only configuration. There is no 200K variant and no [1m] suffix for it. Sessions auto-compact at about 967K tokens by default, and CLAUDE_CODE_AUTO_COMPACT_WINDOW moves that threshold. The gap between 967K and the window ceiling is headroom: the compactor fires before the wall, not at it.

The Claude Code docs carry the 1M boundary and the 967K threshold; per-model default window sizes live on the platform documentation instead. I am not reproducing a per-model table here, because I did not verify one. What the Claude Code docs do state is the boundary: Fable 5, Sonnet 5, Opus 4.6 and later, and Sonnet 4.6 support the 1M window, and you select it with the [1m] suffix on an alias or a full model name.

modelUsage: the receipt for which model ran

The receipt is in the output: with --output-format json, the payload includes a modelUsage field reporting the actual model used, alongside total_cost_usd, a per-model cost breakdown, and the session_id. Aliases resolve by provider, versions gate what a binary can select, an env var can outrank your flag, and a fallback chain can swap models for a turn. After all of that, the model you asked for is a request. In automation I log modelUsage next to the model I requested, and the two disagree often enough to justify the line. When they differ, the explanation is in this chapter’s tables, and the tables beat my memory of them.