TL;DR In
~/.claude/settings.json, add amodelPickerblock with the two or three rows you use and"replaceBuiltInOptions": true. Without thattrue, a row the built-in lineup already covers is skipped and nothing changes.
You open /model and get Default, Fable, Opus, Sonnet, Haiku and the 1M variants, when you only ever switch between two. And if you work through a gateway or Bedrock, the model shows up as a raw ID with an unknown-model warning. Since v2.1.242, the modelPicker setting decides which rows the picker lists, in what order and under which label. Since v2.1.257, behavesAs tells Claude Code which known model sits behind an ID it doesn't recognize.
How it works
modelPicker is an object with an options list and a replaceBuiltInOptions Boolean. Each row carries a required model plus optional label, description and behavesAs. The model field accepts anything --model accepts: an alias such as opus, an Anthropic model ID or an ID in your provider's format.
The detail that decides whether this works at all: with replaceBuiltInOptions at false (the default), your rows are appended after the built-in lineup, and a row whose model is already in that lineup is skipped. List opus and haiku without the true and the picker looks exactly as before. With true, the picker shows only your rows, Default, and one row for the model the session is already running on.
The result, with two rows and the built-in lineup gone (illustrative, the picker is an interactive screen):
> /model
1. Default (recommended)
> 2. Opus (daily) Everything that is not a quick fix
3. Haiku (light work) Scripts, renames, one-off questions
Enter to set as default · s to use this session only
Enter and s keep their usual meaning: Enter saves the model as your default and s uses it for this session only.
Set it up
1. Write your rows in your user settings
In ~/.claude/settings.json:
{
"modelPicker": {
"replaceBuiltInOptions": true,
"options": [
{
"model": "opus",
"label": "Opus (daily)",
"description": "Everything that is not a quick fix"
},
{
"model": "haiku",
"label": "Haiku (light work)",
"description": "Scripts, renames, one-off questions"
}
]
}
}
List order is picker order, except that a grayed-out row drops to the bottom with its reason next to it. Without a label, Claude Code uses the built-in name when it knows the model and the ID otherwise. Without a description, it writes a generic second line. The label changes what you read, not which model runs.
2. Put it where Claude Code reads it
Claude Code reads modelPicker from three places: your organization's managed settings, the --settings flag and your ~/.claude/settings.json. It ignores the key in .claude/settings.json and .claude/settings.local.json, so a cloned repository can't relabel your picker. And it never merges lineups: the highest source that defines the key supplies the whole list. If your company sets one, yours isn't read. Same logic as when two settings files collide.
To try a lineup without touching any file, pass it through --settings:
claude --settings '{"modelPicker":{"replaceBuiltInOptions":true,"options":[{"model":"opus","label":"Opus (daily)"},{"model":"haiku","label":"Haiku (light work)"}]}}'
3. A gateway or Bedrock ID without the unknown-model warning
Start a session on an ID Claude Code doesn't recognize and the startup header says so, assuming a 200K window:
⚠ Model "my-gateway-opus" isn't recognized · context assumed to be 200k tokens · ...
Add the row with behavesAs pointing to the ID of a model your version does know. Claude Code applies that model's capabilities and effort defaults to the row; the label and the ID sent on every request stay as they are:
{
"model": "my-gateway-opus",
"label": "Opus (gateway)",
"behavesAs": "claude-opus-5-5"
}
The alternative is a modelOverrides entry mapping the Anthropic ID to yours. Both get you there; behavesAs keeps it written on the picker row itself.
Reference
| Field | Type | What it does |
|---|---|---|
options[].model |
string, required | Whatever --model takes: alias, Anthropic ID or provider ID |
options[].label |
string | Row text. Doesn't change which model runs |
options[].description |
string | The row's second line |
options[].behavesAs |
string, v2.1.257+ | ID of a known model whose capabilities the row inherits |
replaceBuiltInOptions |
Boolean, false by default |
true hides the built-in lineup; false appends your rows and skips duplicates |
If nothing changes
Check the session startup first. A malformed key doesn't break the file: Claude Code skips it and tells you. This is what it prints when replaceBuiltInOptions arrives as text instead of a Boolean:
These settings entries were skipped because this version of Claude Code can't apply them; the rest of each file is in effect:
--settings (inline JSON) (modelPicker.replaceBuiltInOptions): "replaceBuiltInOptions" must be true or false; received string. This entry was ignored (the rows are added to the built-in lineup).
Three more things that filter your rows:
- A row the session can't serve (a retired model, or one your organization has no access to) is dropped with no notice in the picker. If none survives, the built-in lineup comes back.
- An
availableModelsallowlist still applies: a row outside that list doesn't show. - With
replaceBuiltInOptions: truethe 1M rows disappear too. If you need one,modelaccepts anything--modelaccepts; here is how those variants work.
Official docs: modelPicker
Requirements: modelPicker shipped in v2.1.242 and behavesAs in v2.1.257. Tested on v2.1.303.