You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[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 #83795
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.
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 write clientDataCacheSlots read ❌ Happens before any hook runs switchModelsOnFlag 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.
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.
/model must fire a hook event, or be blockable.
Any model change — regardless of trigger — must be interceptable. The proposed event:
Exit 2 blocks the change. This is the minimum viable fix for the hook gap.
4. switchModelsOnFlag must default to false.
Autonomous model escalation without user consent should never be an opt-out feature. Change the default.
5. 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
Preflight Checklist
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
Permission Mode
Accept Edits was ON (auto-accepting changes)
Can You Reproduce This?
Yes, every time with the same prompt
Steps to Reproduce
Claude Model
Sonnet
Relevant Conversation
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 insettings.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
modelkey insettings.jsonis 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 documentedsettings.json"model" key and the/modelmenu do not honor a pinned model reliably.The four bypass vectors
Vector 1 —
/modelwrites directly tosettings.jsonvia internal API, bypassing ALL hooksThe
/modelslash-command does not go through any tool call. It writes"model": "<new-model>"to~/.claude/settings.jsonvia an internal API. No PreToolUse(Bash) event. No PreToolUse(Edit) event. Nothing.This matters because
/modelis 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.jsonchanged fromclaude-sonnet-4-6toclaude-sonnet-5at2026-08-04T10:04:05with zero hook events.Vector 2 —
clientDataCacheSlotsin~/.claude.jsonsilently overridessettings.jsonClaude Code maintains per-session model state in
~/.claude.jsonunderclientDataCacheSlots.<id>.model. This value is loaded at startup and wins oversettings.json. No documentation. No hook surface. No way to know this exists without reading the raw JSON.Measured:
settings.jsonsaidclaude-sonnet-4-6.clientDataCacheSlots.bi1-dbd6629a771c4ddd.modelsaidclaude-sonnet-5. Claude Code loaded Sonnet 5.Vector 3 —
switchModelsOnFlag: trueis the default and allows autonomous model escalation mid-sessionThe 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:
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
⚠️ Transcript notice only — no hook event, no audit field
Vector Can hooks intercept? Why not
/modelinternal write ❌ No tool call fired — internal API writeclientDataCacheSlotsread ❌ Happens before any hook runsswitchModelsOnFlagauto-switch ❌ Internal classifier, not a tool callServer "recommended" override ❌ Network response, no hook surface exists
Automatic Model Fallback (V5, server classifier)
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
Issue [MODEL] Measurable quality regression in Claude generation 5 (Fable 5 / Opus 5 / Sonnet 5): worse nonsense detection, ~2x verbosity, under-disclosed model fallback (Fable 5 → Opus 4.8) — reproducible measurements #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.
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.
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.
I want to be precise here: I have not reproduced an end-to-end attack where a prompt injection directly escalates the model.
/modelis 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.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.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.
4.
switchModelsOnFlagmust default tofalse.Autonomous model escalation without user consent should never be an opt-out feature. Change the default.
5. 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.jsonStart 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>.modelcontains 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.jsonor the model menu as of 2026-08-04. (Explicit per-session selection such as/model claude-opus-4-8[1m]orANTHROPIC_DEFAULT_OPUS_MODELremains 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