Skip to content

[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

Description

@KeilerHirsch

Preflight Checklist

  • I have searched existing issues for similar behavior reports
  • 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 [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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  2. 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.
  3. /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.
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.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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions