Featured image of post ccswitch Edited the Model, Claude Code Didn't Budge: A Three-Layer Override Root-Cause Diagnosis

ccswitch Edited the Model, Claude Code Didn't Budge: A Three-Layer Override Root-Cause Diagnosis

Editing a provider's model in ccswitch and saving it left Claude Code running the old model. Logs plus a field-by-field database comparison forced out three stacked root causes: (1) edit writes the DB but never pushes live, (2) the DB template drifts from the live file, (3) the ANTHROPIC_MODEL env var hard-pins over the tier mappings. Minimal fix and self-check commands included.

Origin: Three Edits, Zero Effect

I opened ccswitch (v3.19.2, the Windows-side Claude Code config switcher), clicked “edit provider,” changed the Opus-tier model, and saved. Restarted Claude Code — still running the old model. Edited, saved, restarted again — still no change.

“Doesn’t change” problems are the worst kind to guess at. Below is the process of forcing out the root cause with logs and a field-by-field database comparison. The conclusion is more tangled than intuition: the write succeeded; the fault is three things stacked.

First, Clear Two Likely Suspects

Suspect one: write failed (atomic-write os error 50). WSL2 writing Windows-side files over the 9P bridge historically rejects rename, reporting os error 50. But the mtime on ~/.claude/settings.json shows it was just written successfully; ccswitch’s own database was written in the same second, with a pre-switch backup. The write path is clear — ruled out.

Suspect two: WSL and Windows are two different files. Checked: ~/.claude/settings.json is a symlink pointing to the same file on the Windows side. What ccswitch writes on Windows is what Claude Code reads in WSL — one file — ruled out.

Falsifying these two first saves a lot of flailing in the wrong direction. It’s also the first step for any “looks like it didn’t take” problem: confirm whether the write actually happened before fixing things that aren’t broken.

Root Cause One: Three-Layer Model Override

Claude Code decides “which model actually runs” not from one field but from three layers stacked, priority high to low:

  1. ANTHROPIC_MODEL env var — highest priority. Once set, it forces every request to this model, overriding the two layers below.
  2. ANTHROPIC_DEFAULT_<tier>_MODEL — “which concrete model each Opus/Sonnet/Haiku/Fable tier maps to.”
  3. model field — which tier is active (opus/sonnet/haiku/fable).

My config had all three fighting: model: "opus" (Opus tier), Opus tier mapped to model A, but ANTHROPIC_MODEL hard-pinned to model B. The highest layer presses down and the two below go inert — whether you edit the Opus tier mapping or switch the model field, it runs model B.

That’s the first reason “editing had no effect”: you think you’re editing the tier mapping, but the hard-pin sits on top and the tier mapping never gets a word in.

Three-layer model override: ANTHROPIC_MODEL hard-pin overrides tier mappings and the model field

Root Cause Two: ccswitch’s “Edit” and “Apply” Are Two Separate Actions

This the logs forced out. In the time window of this operation, ccswitch’s log only records:

1
2
3
table=settings,  merged_changes=1     ← changed ccswitch's own settings
table=providers, merged_changes=1     ← wrote the providers table (DB template)
(after that only tray mouse events — no "apply to live config" record)

That is: “click edit, change model, save” only writes into ccswitch’s own database providers table (template updated successfully, cloud sync even fired), but does not therefore push the new template into the .claude/settings.json that Claude Code actually reads. To push to live you have to separately click “switch/apply provider.” On startup ccswitch only does a reverse check (should it backfill from live into the DB), never proactively pushing the DB to live.

So: your edit took effect in the database, the live file didn’t move, Claude Code reads the old. Second reason — you did “edit,” not “apply.”

Root Cause Three: DB Template vs. Live File Drift

Pull the template for this provider from the DB and the actual values from the live file, compare field by field — the four tier mappings are all rotated one position off:

Tier / fieldDB template (after edit)live file (actually running)
Opus tierqwen3.8-maxglm-5.2-fast-preview
Sonnet tierglm-5.2-fast-previewdeepseek-v4-flash-0731
Fable tierdeepseek-v4-flash-0731qwen3.8-max
ANTHROPIC_MODELglm-5.2-fast-previewqwen3.8-max

DB template vs live file: four tiers rotated one position off

Not one field wrong — multiple DB edits while live stayed parked at an old switch point, drifting further apart. Even if you go back and click “switch” to make ccswitch push live, it pushes this drifted template and stays messed up. Also note the DB template only contains the env block, not the top-level model and effortLevel fields — that’s ccswitch’s design, but it does make “fully manage models through ccswitch” awkward.

Fix: Delete the Hard-Pin, Let Tiers Speak

Minimal fix: delete the ANTHROPIC_MODEL hard-pin, the highest-priority layer. Once gone, model field picks tier → tier mapping → actual model — that chain finally flows. Paired with sorted tier mappings (Opus=primary, Sonnet=hard tasks, Haiku=light work, Fable=max-score), deleting the pin lets tier division take effect.

After editing you must restart Claude Code — env is injected into the process at startup; editing the file doesn’t affect a running session. The current session keeps old values; fully quit and reopen to read the new config.

How to verify: after restart, use /model to switch to the Sonnet tier and check whether the actual model became the one Sonnet maps to. If it changed, the tier-mapping chain is live.

Daily Path: Skip ccswitch, Edit settings.json Directly

Once diagnosed, the least-effort path is the plainest: don’t use ccswitch day-to-day, edit ~/.claude/settings.json directly. Because the only reason “couldn’t change” happened is ccswitch’s switch overwrites live with the stale DB template; don’t touch it and that overwrite path doesn’t exist — settings.json becomes your single source of truth.

One iron rule: don’t open ccswitch and click “switch/apply provider” again. One click triggers the full overwrite (drifted tier mappings, hard-pin, lost keys all return). Don’t click, edit and restart. For daily tier switching Claude Code’s built-in /model works too — it’s just editing settings.json’s model field, equally persistent under “no ccswitch.”

Honest Boundaries

  • The three-layer priority ordering is static inference plus system-echo corroboration (“settings.json pins X — applies on restart”). Which layer ultimately wins after restart, I didn’t field-test each one — that’s for you to verify on restart.
  • Conclusions target ccswitch v3.19.2. The day of writing, v3.20.0 update was detected; whether it fixes “edit doesn’t push live” is unknown.
  • settings.local.json has higher priority than settings.json. If local also sets a same-named model field, it overrides settings.json — don’t forget to check local when diagnosing.

How to Check Yourself

Reproducing this diagnosis comes down to four commands (swap paths and keys for your own):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 1. See which ANTHROPIC_* env vars the running process actually injected (names only, masked)
env | grep -iE 'claude|anthropic' | sed 's/=.*/=<masked>/'

# 2. See live settings.json's env block and model field
cat ~/.claude/settings.json | python3 -c "import sys,json;d=json.load(sys.stdin);print('model:',d.get('model'));print(json.dumps(d.get('env',{}),indent=2,ensure_ascii=False))"

# 3. See ccswitch's DB template for the current provider (to diff against live)
sqlite3 ~/.cc-switch/cc-switch.db "SELECT settings_config FROM providers WHERE is_current=1"

# 4. See ccswitch's log to find whether "edit save" actually triggered a live write
grep -iE 'table=providers|live|switch|apply' ~/.cc-switch/logs/cc-switch.log | tail -20

Compare the template from step 3 against the live values from step 2, field by field — fields that don’t match are drift points. If step 4’s log has no “apply to live”-type event, that confirms “edit doesn’t push live.”

For any “config edited but didn’t take” problem, the approach is universal: light up every segment of the data flow — did it get written? Is what’s written the same as what’s read? When reading, which priority layer overrode the one you edited? Walk those three and the root cause surfaces on its own.