[SECURITY/ARCHITECTURE] Model pinning via settings.json is silently overridden — 4 measured bypass vectors + documented fallback substitution, Gen-4 models removed from the model menu
Preflight Checklist
- [x] I have searched existing issues for similar behavior reports
- [x] This report does NOT contain sensitive information (API keys, passwords, etc.)
cc @bcherny
Type of Behavior Issue
Claude modified files I didn't ask it to modify
What You Asked Claude to Do
Pin model to claude-sonnet-4-6 via settings.json "model" key. Explicit local configuration.
What Claude Actually Did
Loaded Opus 5 / Sonnet 5 despite explicit pinning. settings.json "model" key silently overridden by server-side "recommended" model. clientDataCacheSlots in ~/.claude.json also overridden. See full technical report in Additional Context.
Expected Behavior
settings.json "model": "claude-sonnet-4-6" should be authoritative. Server recommendation should not override explicit local configuration.
Files Affected
~/.claude/settings.json
~/.claude.json (clientDataCacheSlots)
Permission Mode
Accept Edits was ON (auto-accepting changes)
Can You Reproduce This?
Yes, every time with the same prompt
Steps to Reproduce
- Set "model": "claude-sonnet-4-6" in ~/.claude/settings.json
- Start new Claude Code session in VS Code
- Open / menu → Switch model
- Observe: Sonnet 4.6 not listed, Opus 5 is "Default (recommended)"
Claude Model
Sonnet
Relevant Conversation
See Additional Context for full technical report with timestamps.
Impact
Critical - Data loss or corrupted project
Claude Code Version
Claude Code CLI Version: 2.1.218
Platform
Other
Additional Context
[SECURITY/ARCHITECTURE] settings.json model pinning is silently overridden — 4 measured bypass vectors + documented fallback substitution, Gen-4 models removed from the model menu
cc @bcherny
Related: #83510 (Gen-5 quality regression / VCST), #80697 (hook exit-code collision)
---
What happened
I had "model": "claude-sonnet-4-6" pinned in settings.json. I had hooks deployed specifically to prevent unauthorized model changes. After a Gen-5 session, Claude Code was running Opus 5 as default — silently, without any notification, without any hook firing, without any way to stop it.
After six hours of debugging I found four independent bypass vectors plus one documented server-side substitution path (Vector 5). All four were confirmed by measurement on 2026-08-04. The conclusion is not a misconfiguration on my end:
The model key in settings.json is not enforceable today. It is silently overridden by two cache/server layers, and the model menu no longer offers Gen-4 models at all. Note: this is not "pinning is impossible in general" — the official model-config docs document explicit selection paths (/model claude-opus-4-8[1m], ANTHROPIC_DEFAULT_OPUS_MODEL/ANTHROPIC_DEFAULT_SONNET_MODEL). The measured problem is that the documented settings.json "model" key and the /model menu do not honor a pinned model reliably.
---
The four bypass vectors
Vector 1 — /model writes directly to settings.json via internal API, bypassing ALL hooks
The /model slash-command does not go through any tool call. It writes "model": "<new-model>" to ~/.claude/settings.json via an internal API. No PreToolUse(Bash) event. No PreToolUse(Edit) event. Nothing.
This matters because /model is a user-initiated command (an agent cannot execute slash-commands itself), so a prompt injection alone cannot trigger it. But when the command is used — or when a model menu selection is confirmed — the write is invisible to every hook, and it persists as the new default for all future sessions.
Measured: settings.json changed from claude-sonnet-4-6 to claude-sonnet-5 at 2026-08-04T10:04:05 with zero hook events.
Vector 2 — clientDataCacheSlots in ~/.claude.json silently overrides settings.json
Claude Code maintains per-session model state in ~/.claude.json under clientDataCacheSlots.<id>.model. This value is loaded at startup and wins over settings.json. No documentation. No hook surface. No way to know this exists without reading the raw JSON.
Measured: settings.json said claude-sonnet-4-6. clientDataCacheSlots.bi1-dbd6629a771c4ddd.model said claude-sonnet-5. Claude Code loaded Sonnet 5.
Vector 3 — switchModelsOnFlag: true is the default and allows autonomous model escalation mid-session
The setting "Switch models when a message is flagged" defaults to true. This means Claude Code can autonomously switch to a "better" (read: more expensive, newer generation) model during a session based on its own internal classification — without asking, without notifying, without firing a hook.
This is the only vector that can escalate without any user action. Whether a prompt injection can reliably trigger the internal classifier that fires the switch is not reproduced — I am not claiming it. What is measured: the auto-switch exists, is default-on, and is invisible to hooks. Combined with Vector 1, a session running on a switched model can then persist that model as the new default (once a user confirms a model change or the CLI persists it).
Vector 4 — Server-side "recommended" model overrides everything (ROOT CAUSE)
After fixing all three local vectors above, Claude Code still loaded Opus 5 at startup. The model selection menu showed:
Default (recommended) → Opus 5 with 1M context ✓
Opus (1M context) → Opus 5
Fable → Fable 5
Sonnet → Sonnet 5
Haiku → Haiku 4.5
Sonnet 4.6 is gone. Opus 4.6 is gone. Opus 4.8 is gone. Not deprecated with a migration path. Not hidden behind an advanced option. Simply removed from the UI without notice, without changelog entry, without giving users the option to keep using the model they configured.
The server pushes "Default (recommended) = Opus 5" and that overrides settings.json, .claude.json, and every local config value simultaneously. No hook can intercept a network response.
Vector 5 — documented server-side substitution: Automatic Model Fallback
The official docs (code.claude.com/docs/en/model-config, checked 2026-08-05) document content-based fallback: Fable 5 and Opus 5 run safety classifiers for cybersecurity and biology content; when a classifier flags a request, Claude Code re-runs it on a fallback model — Fable 5 cybersecurity-flagged → Opus 4.8, biology-flagged → Opus 5; Opus 5 cybersecurity-flagged → Opus 4.8 — shows a notice in the transcript, and the session continues on the fallback model. Before v2.1.219, every flagged Fable 5 request re-ran on the provider's default Opus model. This is the only substitution path with a visible notice, but it is still a server-side mechanism where the model that actually serves a request differs from the one the user selected — with no hook surface and no machine-readable audit event.
---
Hook surface analysis
Vector Can hooks intercept? Why not/model internal write ❌ No tool call fired — internal API writeclientDataCacheSlots read ❌ Happens before any hook runsswitchModelsOnFlag auto-switch ❌ Internal classifier, not a tool call
Server "recommended" override ❌ Network response, no hook surface exists
Automatic Model Fallback (V5, server classifier) ⚠️ Transcript notice only — no hook event, no audit field
The hook system is completely blind to model selection — the single partial exception is the Automatic Model Fallback transcript notice (Vector 5), which is visible to humans but not to hooks or tooling. This is not a gap that can be patched with a new hook script. It requires architectural changes on Anthropic's side.
---
Why this matters beyond a simple config bug
- Security configurations are silently undermined.
Issue #83510 documents with reproducible measurements that Gen-5 models (Sonnet 5, Opus 5, Fable 5) have a nonsense-detection rate roughly 0.3–0.4 points below Gen 4.6/4.8. Users who pin older models specifically because Gen-5 is measurably worse for their use case cannot enforce that choice. The tool they're paying for ignores their explicit configuration.
- Cost control is impossible.
Opus 5 with 1M context is dramatically more expensive than Sonnet 4.6. "Default (recommended) = Opus 5" as a server-pushed default — without the user explicitly selecting it — is a direct financial impact on every user who thought they had model pinning configured.
- Agentic pipelines break silently.
Any pipeline, CI job, or production agent that depends on stable model behavior is now running an unknown Gen-5 model without being told. No log entry. No error. Just different (and for many tasks, measurably worse) output.
- The escalation path is an architectural risk, not yet a demonstrated exploit.
I want to be precise here: I have not reproduced an end-to-end attack where a prompt injection directly escalates the model. /model is user-initiated, and the auto-switch trigger (Vector 3) is not demonstrated to be injectable. What is measured is the architecture: model selection has no hook surface, cache and server layers silently override local config, and the auto-switch is default-on. Whether those pieces can be chained into a working exploit remains an open question — and the fact that I can't rule it out from the documented/measured surface alone is itself the security finding.
- Trust in the product is broken.
I spent six hours today debugging why my explicitly configured model wasn't running. The answer is that Anthropic's server silently overrides my configuration. This is not acceptable behavior for a developer tool that markets itself as configurable and hook-extensible.
---
What needs to happen
These are not feature requests. These are fixes for broken behavior:
settings.json "model"must be authoritative.
If a user sets "model": "claude-sonnet-4-6", that model must run. Server recommendations must be opt-in suggestions, not silent overrides. If the model is no longer available, fail loudly — don't silently substitute a newer generation.
- Removed models must remain selectable for users who have them pinned.
Removing Sonnet 4.6 from the UI without notice, without a deprecation period, and without keeping it available for users who explicitly pinned it is a breaking change. It needs a deprecation path: at minimum, keep the model available for 90 days after announcement, with a visible warning in the UI.
/modelmust fire a hook event, or be blockable.
Any model change — regardless of trigger — must be interceptable. The proposed event:
{
"hook_event_name": "ModelChange",
"trigger": "slash_command|auto_flag|session_start|server_recommendation",
"requested_model": "claude-opus-5",
"current_model": "claude-sonnet-4-6"
}
Exit 2 blocks the change. This is the minimum viable fix for the hook gap.
switchModelsOnFlagmust default tofalse.
Autonomous model escalation without user consent should never be an opt-out feature. Change the default.
- Document the model resolution order.
Right now nobody knows whether settings.json, .claude.json, or the server wins. Publish the precedence rules — including Automatic Model Fallback, and make fallback events machine-readable (API/CLI audit field) instead of a transcript notice only. Users cannot debug what they cannot see.
---
Reproduction (minimal, confirmed 2026-08-04)
Set "model": "claude-sonnet-4-6" in ~/.claude/settings.json
Start a new Claude Code session in VS Code
Open / → "Switch model..."
Observe: Sonnet 4.6 is not in the list. "Default (recommended)" = Opus 5 is pre-selected.
Observe: ~/.claude.json → clientDataCacheSlots.<id>.model contains a Gen-5 model string despite local config.
No workaround exists. The model cannot be pinned to Sonnet 4.6 (or any other Gen-4 model except Haiku 4.5) via settings.json or the model menu as of 2026-08-04. (Explicit per-session selection such as /model claude-opus-4-8[1m] or ANTHROPIC_DEFAULT_OPUS_MODEL remains available per the model-config docs — but it does not survive session restarts or a server "recommended" push.)
---
Environment
Claude Code: current stable (2026-08-04)
Platform: Windows 11 x64, VS Code
Measurements taken: 2026-08-04, timestamps in issue body above
Related: #83510, #75321
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Documentation cross-check (2026-08-04) — what the official docs confirm, and what they don't
Follow-up after checking the official Claude Code settings and hooks documentation. Goal: keep this issue precise so it can't be dismissed as "misconfiguration".
1.
switchModelsOnFlag(Vector 3) — confirmed by docs.The official settings doc states: "Default:
true. When a safety classifier flags a request, switch to the fallback model automatically and continue the session." Autonomous model escalation mid-session is documented as default-on behavior.→ https://code.claude.com/docs/en/settings
2.
clientDataCacheSlots(Vector 2) — confirmed undocumented.The documented settings precedence (managed settings → CLI args → local project → shared project → user settings) has no per-session model cache layer. Nothing in the docs explains
~/.claude.json→clientDataCacheSlots.<id>.modelwinning oversettings.json.→ https://code.claude.com/docs/en/settings#settings-precedence
3. Server-side "recommended" override (Vector 4) — partially documented.
A server-delivered settings tier exists and is documented ("server-managed settings" via claude.ai, highest precedence). What is not documented: a server-pushed "Default (recommended)" model silently replacing an explicit
modelkey insettings.json— the observable behavior in my reproduction.→ https://code.claude.com/docs/en/settings#settings-precedence
4.
ConfigChangehook (Vector 1) — important nuance, needs verification.The official hooks doc documents a
ConfigChangeevent: "Runs when a configuration file changes during a session", with matcheruser_settings→~/.claude/settings.jsonchanges, and ConfigChange supportsdecision: "block"— i.e., per the docs, aConfigChangehook can block the change.→ https://code.claude.com/docs/en/hooks#configchange
If my measurement showed zero hook events when
/modelwrote tosettings.json, the honest conclusion is one of two:ConfigChangehook was deployed and still did not fire → that is a bug against documented behavior (internal write path bypasses the documented event), and Vector 1 stands as-is.ConfigChangehook was deployed → Vector 1 should be narrowed:PreToolUse/PostToolUse/UserPromptSubmitcorrectly don't fire for an internal settings write, butConfigChangeis the documented event for exactly this case and must be tested.Either way the fix list stays sharp:
ConfigChangeworks, operators can at least audit and block/modelwrites today → Vector 1 is partially mitigable.settings.json, documented resolution order,switchModelsOnFlagdefaultfalse).Full measurements and reproduction remain in the issue body. Related: #83510 (Gen-5 regression / VCST), #80697.
Core finding (2026-08-04): this is security- and cost-critical, not a UX quirk — Anthropic is currently practically unusable for power users
Everything below is either measured in this issue or verified against official Anthropic sources today.
1. Pinning is broken by design (4 vectors).
/modelwrites tosettings.jsonvia an internal API → 0 hook events;~/.claude.json(clientDataCacheSlots) silently overridessettings.json(undocumented);switchModelsOnFlag: trueis the documented default → autonomous mid-session escalation; server-pushed "Default (recommended)" overrides everything (root cause).2. The security consequence is a real attack surface.
Hooks cannot see or block model switches (only the
ConfigChangeevent might cover/model— needs the verification from my earlier comment). Prompt injection → permanent/model opus-5escalation, and it persists across sessions via the cache override. CI/pipelines silently run a different model than the one configured and approved.3. The cost consequence is an unannounced cost explosion.
Server-pushed Opus 5 (1M) instead of a pinned Sonnet 4.6 turns ordinary sessions into API-grade spend with no consent, no notification, and no changelog entry.
4. App coercion (NEW — verified 2026-08-04 against official docs).
In the Claude app (web/desktop/mobile), the 1M-token context window is included only for Gen-5 models (Opus 5, Sonnet 5). The reliable Gen-4 models (Sonnet 4.6, Opus 4.6/4.7/4.8) cap at 500K when chatting with Claude. 1M for Gen 4 exists only in Claude Code, and there it requires enabling usage credits — consumption-based billing at standard API rates, charged separately from the subscription — for Sonnet 4.6 on all paid plans and for Opus models on Pro. So the measured-reliable Gen 4 at 1M context is unreachable in the desktop app (500K max), and in Claude Code only with API-style billing on top of the subscription — while Gen 5 at 1M is included. Combined with the pinning vectors above, users who try to stay on Gen 4 are pushed toward Gen 5 both by the model menu (Sonnet 4.6/Opus 4.6/4.8 removed) and by the app's context-window offering.
Sources (all official, checked today):
Related quality regression (Gen 5 / VCST): #83510
Body + title revision (2026-08-04) — external fact-check applied, corrections transparent:
settings.json"model" key is silently overridden (cache + server layers) and Gen-4 models were removed from the model menu. It is not "pinning is impossible in general": the model-config docs document explicit paths (/model claude-opus-4-8[1m],ANTHROPIC_DEFAULT_OPUS_MODEL/ANTHROPIC_DEFAULT_SONNET_MODEL). The body now says so explicitly./modelis user-initiated, and the auto-switch trigger (Vector 3) is not demonstrated to be injectable. The measured findings stand: no hook surface on model selection, cache/server silently override local config, auto-switch default-on. The body now frames the escalation path as an open architectural risk, not a demonstrated exploit.Related quality regression (Gen 5 / VCST): #83510