availableModels in managed settings is not enforced for automatic model switching (flagged-message switch runs a blocked model)
Environment
- Claude Code CLI 2.1.198, macOS (Darwin 25.5.0)
- Managed settings at
/Library/Application Support/ClaudeCode/managed-settings.json, deployed 2026-07-04 22:09 JST (verified via file mtime), containing:
"availableModels": ["fable", "claude-fable-5", "claude-opus-4-6", "sonnet", "haiku"]
Note: claude-opus-4-8 is absent in both alias form (opus) and concrete-ID form, so under either alias-resolution semantics it should be blocked.
ANTHROPIC_DEFAULT_OPUS_MODELwas NOT set at the time of the events below (it was added afterwards as a mitigation).- Session model:
claude-fable-5(selected via/model), automatic model switching on flagged messages enabled.
What happened
When a message was flagged and Claude Code automatically switched models away from Fable 5, the session continued on claude-opus-4-8 — a model not in the availableModels allowlist. This happened 3 times in one day (starting 14:28 / 15:43 / 17:16 JST on 2026-07-05, i.e. ~16 hours after the managed settings were deployed), for a cumulative ~3.8 hours.
We also verified that no local configuration selected this model: at the time of the events, no .claude/settings.json in the workspace referenced claude-opus-4-8 (the session model was claude-fable-5, the workspace default claude-opus-4-6[1m]), and no local tooling writes the managed-settings file. The switch target was chosen by Claude Code itself — which is precisely the path the documentation says must be checked against availableModels. After each flagged episode the session returned to claude-fable-5, so the cycle repeated without surfacing any policy warning.
Manual switching is enforced correctly: we verified on 2026-07-04 (before these events, with the same managed settings in place) that selecting a blocked model via /model is refused. Only the automatic switch path bypasses the allowlist, and the deployment-to-event gap rules out settings-propagation timing.
How we measured
Counted model fields in the session transcript JSONL (~/.claude/projects/<project>/<session>.jsonl), selecting .type=="assistant" events with .message.model=="claude-opus-4-8" within the incident day: 785 events in three clusters — 14:28–15:08, 15:43–16:45, and 17:16–19:24 JST (cumulative ~3.8 hours); all other events are claude-fable-5 or claude-opus-4-6. We can share redacted transcript lines (model/timestamp fields only) on request.
What I expected
The model configuration documentation (https://code.claude.com/docs/en/model-config.md) states, under "Restrict model selection":
"Automatic model changes are checked the same way: elements of a fallback model chain outside the allowlist are dropped, a plan-mode upgrade such as opusplan to an excluded model is skipped so planning continues on the session's model, and an automatic model fallback whose target is excluded does not run, so the flagged request ends with a refusal instead."
and under "Automatic model fallback":
"The fallback target is checked against availableModels. When it is blocked, no fallback occurs. The refusal is shown as a normal error and the session's model is unchanged."
The observed behavior contradicts both: the automatic switch executed onto the excluded model and the request did not end with a refusal.
Why it mattersavailableModels is the mechanism admins use to pin teams to approved models (cost and policy control). An unattended agent session can spend hours on a model the administrator explicitly excluded, with no warning surfaced. Transcript-level auditing was the only way we noticed.
Reproduction (as observed)
- Deploy managed-settings.json with an
availableModelsallowlist that excludesclaude-opus-4-8(full file as above). Restart the Claude Code session so the policy is active; confirm enforcement by attempting/model claude-opus-4-8(refused). - Set the session model to
claude-fable-5and keep automatic switching on flagged messages enabled. - Work with content that triggers a flagged-message switch. In our case this was legitimate security-engineering material (we cannot share the exact prompts); the switch itself is visible in the UI notice and in the transcript as a change of the assistant
modelfield. - Observe the transcript: subsequent assistant events run on
claude-opus-4-8despite the allowlist, until the session switches back.
Related issues (searched before filing — no duplicate found)
- #67009 (refusal fallback is sticky for the rest of the session) is adjacent but does not cover the allowlist-bypass aspect reported here.
- Context: the changelog shows
availableModelsenforcement being hardened incrementally around v2.1.175–v2.1.176 (default-model constraint,ANTHROPIC_DEFAULT_*_MODELbypass fix,/fasttoggle fix). The automatic-switch path appears to be a remaining gap in that same enforcement family.
Related observation (minor, can file separately)
With ANTHROPIC_DEFAULT_OPUS_MODEL=claude-opus-4-6 set (our mitigation so that any automatic switch lands on an allowed model), the /model picker renders the Opus row with the generation-default label (4.8) instead of the env-effective value (4.6). Actual API calls honor the env var (transcript verified); only the picker label is affected. Happy to file this as a separate issue if preferred.