[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

Status Open
Reported on v2.1.218
Maintainer reply None cached
Activity 10 comments · opened Aug 4, 2026

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

  1. Set "model": "claude-sonnet-4-6" in ~/.claude/settings.json
  2. Start new Claude Code session in VS Code
  3. Open / menu → Switch model
  4. 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.
---
Why this matters beyond a simple config bug

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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:

  1. 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.

  1. 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.

  1. /model must 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.

  1. switchModelsOnFlag must default to false.

Autonomous model escalation without user consent should never be an opt-out feature. Change the default.

  1. 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.jsonclientDataCacheSlots.<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

View original on GitHub ↗

3 Comments

KeilerHirsch · 26 days ago

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.jsonclientDataCacheSlots.<id>.model winning over settings.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 model key in settings.json — the observable behavior in my reproduction.
https://code.claude.com/docs/en/settings#settings-precedence

4. ConfigChange hook (Vector 1) — important nuance, needs verification.
The official hooks doc documents a ConfigChange event: "Runs when a configuration file changes during a session", with matcher user_settings~/.claude/settings.json changes, and ConfigChange supports decision: "block" — i.e., per the docs, a ConfigChange hook can block the change.
https://code.claude.com/docs/en/hooks#configchange

If my measurement showed zero hook events when /model wrote to settings.json, the honest conclusion is one of two:

  • A ConfigChange hook 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.
  • No ConfigChange hook was deployed → Vector 1 should be narrowed: PreToolUse/PostToolUse/UserPromptSubmit correctly don't fire for an internal settings write, but ConfigChange is the documented event for exactly this case and must be tested.

Either way the fix list stays sharp:

  • If ConfigChange works, operators can at least audit and block /model writes today → Vector 1 is partially mitigable.
  • Vectors 2 and 4 (cache override, server "recommended" override) have no hook surface regardless, and still require the architectural changes proposed above (authoritative settings.json, documented resolution order, switchModelsOnFlag default false).

Full measurements and reproduction remain in the issue body. Related: #83510 (Gen-5 regression / VCST), #80697.

KeilerHirsch · 26 days ago

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).
/model writes to settings.json via an internal API → 0 hook events; ~/.claude.json (clientDataCacheSlots) silently overrides settings.json (undocumented); switchModelsOnFlag: true is 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 ConfigChange event might cover /model — needs the verification from my earlier comment). Prompt injection → permanent /model opus-5 escalation, 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

KeilerHirsch · 26 days ago

Body + title revision (2026-08-04) — external fact-check applied, corrections transparent:

  1. Title/claim sharpened. The precise, measured claim is: the 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.
  2. Prompt-injection claim downgraded. I have not reproduced an end-to-end exploit where an injected prompt escalates the model — /model is 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.
  3. Everything else (4 vectors, hook-surface table, measured timestamps, fix proposals) unchanged.

Related quality regression (Gen 5 / VCST): #83510

Showing cached comments. Read the full discussion on GitHub ↗