If your org locked Claude Code to "claude-opus-5" in availableModels sometime this year, you probably assumed that was the model your engineers were running. It wasn’t, not exactly. By default, an availableModels entry doesn’t pin a version — it pins a family. "claude-opus-5" permits Opus 5 and every later release that extends it, including Opus 5.5, the moment Anthropic ships it. No settings file changed. No one approved it. It just became selectable.
Claude Code v2.1.283 (September 25, 2026) shipped two managed-only settings that finally close that gap: deniedModels and availableModelsMatch: "exact". Coverage so far has mostly restated the changelog line. This is what the gap actually looked like in practice, how the two new settings interact with the two that already existed, and working managed-settings.json templates for the scenarios that actually come up.
The gap, concretely
Here’s the failure mode. A compliance team writes this in March, reviews it, ships it to every laptop via MDM:
{
"availableModels": ["claude-opus-5", "haiku"]
}
The intent is “our agents run Opus 5, full stop — that’s the model we validated for this workflow.” The actual behavior, per Anthropic’s own docs: “A version prefix matches later model IDs that extend it, so claude-fable-5 permits both Fable 5 and Fable 5.1, while claude-fable-5-1 permits only Fable 5.1.” Swap in Opus, and "claude-opus-5" was never a pin — it was a prefix match against everything in that release line.
Six months later Anthropic ships Opus 5.5. It fits the prefix. It’s now in every engineer’s model picker, running against a managed-settings.json nobody touched, with zero entries in whatever change log your MDM keeps. If your audit trail assumes “a model only becomes available when someone edits availableModels,” that assumption was wrong the whole time — the assumption was actually “a family only becomes available when someone edits it,” and Anthropic controls when new members join the family.
This isn’t a bug. It’s documented, intentional behavior, and it makes sense for the common case (most teams want automatic minor-version rollups). It’s just not what most availableModels configs actually communicate to the person reading them.
What v2.1.283 adds
Two new managed-only settings, both requiring Claude Code v2.1.283 or later:
availableModelsMatch: "exact" — changes the matching rule itself. With it set, every availableModels entry permits only the exact version it names. New releases in that family stay blocked until someone explicitly adds them.
{
"availableModels": ["claude-opus-5", "haiku"],
"availableModelsMatch": "exact"
}
Now Opus 5.5 is blocked the day it ships, same as everything else not on the list. Turning it on is the fix for the gap above — but it’s opt-in, and it applies to the whole availableModels array, not per-entry.
deniedModels — a separate deny list that blocks specific models even when availableModels would otherwise permit them. This is for the opposite problem: you’re fine with the family staying open, but one specific release needs to be off-limits (a model your security team flagged, a version with a known regression in your eval suite, a point release you haven’t validated yet).
{
"availableModels": ["opus", "sonnet"],
"deniedModels": ["claude-opus-5-5"]
}
A family alias in deniedModels blocks the whole family the same way it does in availableModels — "opus" in deniedModels blocks every Opus version, not just one.
The precedence table nobody’s published yet
There are now four settings that touch model selection, and they don’t all live in the same scope. Official docs cover them one at a time; here’s how they resolve together when they disagree:
| Setting | Scope | What it does | Wins against |
|---|---|---|---|
availableModels | Any file (managed overrides all) | Defines the allowlist | Nothing below it in the merge order |
availableModelsMatch | Managed only | Switches allowlist matching from prefix to exact | N/A — modifies how availableModels is read, doesn’t compete with it |
deniedModels | Managed only | Hard-blocks specific models or families | Always wins, even over an availableModels entry that explicitly allows the same model |
enforceAvailableModels | Any file | Forces the /model Default option to resolve inside the allowlist | Only affects what “Default” means, not what’s selectable |
The one line worth memorizing: deniedModels beats availableModels unconditionally. If a model is in both lists, it’s blocked. There’s no scenario where an explicit allow overrides an explicit deny — which is the correct default for a security control, but it also means a deniedModels entry someone forgot about will silently override an availableModels change someone makes later, with no error and no warning. Document both files in the same place, or this becomes a debugging session six months from now.
availableModelsMatch isn’t really a third competitor in this table — it’s a modifier on how availableModels itself is parsed. Without it, availableModels entries are prefix matches. With it, they’re exact matches. deniedModels behaves the same way either way: it’s always exact-or-family per entry, unaffected by whether availableModelsMatch is set.
Where these settings actually live
All four are managed-settings.json fields (the top two are managed-only; the bottom two also work from lower scopes but managed always wins). Managed settings are not .claude/settings.json in your repo — they live in an OS-level system path that only an admin can write to:
| OS | Path |
|---|---|
| macOS | /Library/Application Support/ClaudeCode/managed-settings.json |
| Linux / WSL | /etc/claude-code/managed-settings.json |
| Windows | C:\Program Files\ClaudeCode\managed-settings.json |
If more than one team owns a piece of the policy, you don’t have to fight over one file: drop additional fragments in a managed-settings.d/ directory next to managed-settings.json, and Claude Code merges managed-settings.json first, then every *.json file in that directory alphabetically. That matters here specifically because deniedModels and availableModels are easy to end up in different fragments owned by different teams (security vs. platform), and the merge order determines which fragment’s entries a reader sees first when debugging — not which one “wins,” since deny always wins regardless of file order.
Because these are managed-only, nothing in your repo’s .claude/settings.json or a developer’s ~/.claude/settings.json can add to or shrink the list. A common point of confusion: a project’s settings.json cannot extend the availableModels list even by adding a model the team actually needs — the setting only applies when it comes from managed settings, so any availableModels entry in project or user scope is simply ignored while a managed one is active.
Three config templates
Regulated environment — pin to one exact version, block everything else in the family:
{
"availableModels": ["claude-sonnet-4-5", "haiku"],
"availableModelsMatch": "exact",
"enforceAvailableModels": true
}
Nothing outside claude-sonnet-4-5 (the exact version) or haiku is selectable, and the /model Default option can’t resolve to something outside that list either. This is the config for a workflow that went through model-specific validation and can’t silently drift when Anthropic ships a point release.
Family stays open, one bad release is blocked:
{
"availableModels": ["opus", "sonnet"],
"deniedModels": ["claude-opus-5-5"]
}
Use this when your eval suite flags a regression in a specific point release but you don’t want to freeze the whole family while you wait for the fix — new Opus releases after 5.5 are still permitted automatically.
Full governance stack, matching the official “complete example” but with the version gap closed:
{
"model": "claude-sonnet-4-5",
"availableModels": ["claude-sonnet-4-5", "haiku"],
"availableModelsMatch": "exact",
"enforceAvailableModels": true,
"deniedModels": [],
"env": {
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-5"
}
}
deniedModels: [] here is deliberate, not decorative — it documents that the deny list was considered and is intentionally empty, rather than leaving a future reader to guess whether it was forgotten.
Put it in CLAUDE.md too, not just managed-settings.json
None of this is visible to a contributor reading your repo’s CLAUDE.md — managed-settings.json lives outside the repo entirely, on the machine, not in git. If a teammate opens /model and finds the picker restricted, or a subagent silently runs on a model they didn’t expect, they have no way to find out why from the codebase. A short note in CLAUDE.md closes that loop:
## Model policy
This org pins Claude Code to `claude-sonnet-4-5` (exact version, no auto-upgrade)
via managed-settings.json. If `/model` looks restricted, that's why — see
#platform-eng, not a local settings.json.
That’s a one-time addition, and it’s the difference between “why is my model picker broken” turning into a support ticket versus a five-second read.
For the rest of Claude Code’s settings.json fields — permissions, sandboxing, hooks — see our complete settings.json field reference, which covers the precedence order across all five config scopes these four settings sit inside of. And if you’re assembling a CLAUDE.md or managed-settings baseline for your own team, our gallery has real examples across frameworks and tools to start from.