[BUG] /model and /effort mutate global settings.json — breaks agents/fleet view; no way to configure per-agent model or effort
Summary
/model and /effort write to ~/.claude/settings.json immediately and globally. In the agents/fleet view (claude agents) this means there is no supported way to run a fleet of agents with intentional, independent model and effort configurations.
Environment
- Claude Code: 2.1.169
- Platform: Windows 11
The problem
~/.claude/settings.json is a single shared file. Every Claude Code session reads from it on start. /model and /effort both write to it immediately on invocation.
The slash commands look and feel like session controls, not "save my settings" commands. In a single session this is a confusing default. In the agents/fleet view it becomes a correctness bug:
- You have three agents running: one on Opus doing deep analysis, one on Sonnet doing fast search, one on Haiku doing summarisation.
- The Sonnet agent completes a phase and a skill invokes
/effort high. - Any agent started after that point inherits
higheffort on whatever model that last write left behind. - There is no warning. There is no way to know which agent last mutated the shared state.
Launch ergonomics compound this: when you spawn a new agent from the fleet view, it inherits whatever settings.json currently contains — not the model or effort you intended when you hit "new agent." There is no way to launch an agent with an explicit, intentional model/effort configuration other than CLI flags, which must be known before the session starts and cannot be changed once running.
Note: v2.1.169 fixed background agents ignoring ANTHROPIC_MODEL env values — confirming that even the env var workaround for model configuration was broken for background agents until yesterday.
Steps to reproduce
- Open
claude agents - Launch agent A on Opus — note current model/effort
- Launch agent B — in that session run
/modeland select Sonnet - Launch agent C from the fleet view
- Agent C starts on Sonnet, not Opus
Expected behaviour
/modeland/effortshould be session-local by default and should not write tosettings.json.- A distinct, explicit action — e.g.
/model save,--saveflag, or a dedicated/settings set modelcommand — should be required to persist a change. - When launching a new agent from the fleet view, the model and effort should be specifiable at launch, independently of any global default.
- Each agent in the fleet view should maintain its own model/effort state for its lifetime.
Related issues (all closed as stale or duplicate without a fix)
- #20745 — Model setting changes globally across all sessions (regression)
- #27608 — Status bar shows stale model after settings.json change from another session (closed as duplicate)
- #27627 —
/modelcommand is global, not per-session (closed as duplicate of #27608) - #36288 —
/modelaffects all concurrent sessions instead of being per-session (closed as duplicate) - #37303 —
effortLevelnot persisted for Max mode; model/effort have no per-session isolation - #47820 —
/effortsession command does not overrideeffortLevelinsettings.json - #49076 —
/modelsilently persistseffortLeveltosettings.json, overriding user's preferred default (closed as duplicate) - #49166 —
/effortis global across concurrent sessions instead of session-scoped - #53416 —
/effortsetting is global across sessions instead of per-session (closed as duplicate of #49166) - #65598 — feat: support effort level in agent definition frontmatter (open — feature request for the same gap)
- #66346 —
effortLevelinsettings.jsonnot honoured on session start (open)
This issue is not a duplicate of any of the above. Those issues cover single sessions and concurrent terminal sessions. This issue covers the agents/fleet view, where the lack of per-agent launch configuration means there is no supported way to run agents with intentional, independent model and effort settings.
9 Comments
We hit this exact issue running multi-agent fleets — every session writing to shared
settings.jsoncreated a race condition where agent A's model choice would leak into agent B's next startup.Our workaround: inject model+effort via frontmatter in the agent definition itself and defensively re-assert via
/modelon every session init. Not clean, but it prevents the shared-state churn.The root issue is that
/modeland/effortlook like imperative session commands but have declarative persistence semantics. You expectset model for THIS session, you getSAVE model as new default for ALL sessions.A clean fix would be:
The agents/fleet view amplifies this: you want agent A on Opus analyzing, agent B on Haiku summarizing, agent C on Sonnet routing — but the last
/modelcall wins globally. There's no launch-time model override in the fleet UI, so you're stuck with CLI flags (which can't change mid-session) or living with the shared state churn.Related: our self-healing hook pattern works around similar cross-session state issues by treating
settings.jsonas append-only and using per-agent namespaces. But model/effort can't use that pattern since they're singletons.---
We build multi-agent systems with SwarmAI. Discussion: T-CUL: Cultivation > Config
The root problem is that
/modeland/effortare presented as session controls but act as account-level settings writes. In a single-agent workflow that ambiguity is invisible. In a fleet it surfaces as a race condition the moment two agents are running concurrently — agent A picks/modeland agent B picks it up on its next startup.The
settings.jsoninjection workaround that xg-gh-25 describes above is the right short-term path: push model/effort into agent-definition frontmatter and treatsettings.jsonas read-only at runtime. One addition worth trying: if you scope the frontmatter injection to a per-agent working directory (giving each agent an isolated~/.claude/equivalent), you also eliminate the restart-required discovery issue from #66327. The two issues share the same root: a single shared config file being used as both a persistent preference store and a runtime session signal.The feature ask that would solve this cleanly:
/modeland/effortshould write to a session-scoped config layer, not globalsettings.json. The global file becomes the default; session-level overrides live in memory or a session-specific file. That's the architecture that would make fleet management workable without workarounds.I routinely keep several Claude Code sessions open at once across different projects (e.g. a frontend and a backend repo), and I adjust effort dynamically within a single session depending on the task at hand: low/medium for a quick change, then high for a complex refactor a few minutes later. This is the core of how I work — effort is a per-task dial, not a per-launch decision.
The current behavior makes that impossible without cross-contamination:
Repro:
CLI launch flags (--effort, CLAUDE_CODE_EFFORT_LEVEL) don't solve this, because they fix effort at startup — the whole point is changing it mid-session. The only workaround is an isolated CLAUDE_CONFIG_DIR per session, which also forks auth/MCP/hooks and is far too heavy for everyday use.
The root cause is the one already called out above: /model and /effort look like imperative session controls ("set effort for THIS session") but have declarative, account-global persistence semantics ("save as new default everywhere"). In single-session use the ambiguity is invisible; with any concurrency it's a correctness bug.
Proposed minimal fix — mirror the existing fastModePerSessionOptIn:
{ "effortLevelPerSessionOptIn": true }
When set, /effort (and ideally /model) apply to the current session only and do not write to settings.json. This already has precedent in the codebase, keeps the default behavior unchanged for those who rely on it, and fully fixes both the interactive multi-session and the fleet/agents cases.
Worth noting this is a regression (#56262): /effort used to be per-session and reset on next launch. Restoring that — even behind the opt-in flag above — would unblock a lot of daily power-user workflows.
I discovered this when a complex session using Fable switched itself to Haiku and suddenly became very confused. 😆
I propose this to restore sane behavior:
--model/--effortflags can override this./modelshould NOT write to the global config. That command should be for changing the current model. Perhaps add an additional/model-defaultor a flag for setting it globally, as suggested by others.It also seems that Claude itself has autonomously closed a bunch of these as duplicates, linking them to a stale one that had no actual discussion or action. Bad bot! Don't do that.
+1. Hit this today in a plain interactive session, not the fleet view. Ran
/effort highfor one heavy task and it silently saved as my default for new sessions. The only session-scoped path is startup flags, which can't help mid-session. Making/modeland/effortsession-only by default, with an explicit flag to persist, would cover both the single-session and fleet cases.Does this item from the change log for v2.1.211 partially fix this?
Without knowing the mechanism, that bug could have been related to this. If subagents were fixed without addressing the underlying issue, though, it may have just been patching a symptom without fixing the root cause.
The main issue is the leakage of state between sessions. And in the interest of saving myself a few minutes, here's Fable 5's own words. 😏
Relevant design principles it runs afoul of:
Its 5th point is addressed (the UI does at least briefly say it changed the default) but that's a weak assertion on top of a bad design. Fixing the design is the better choice.
Adding a version-control angle on the same root cause (
/modelwriting tosettings.json), distinct from the fleet-view cases above.When
settings.jsonis git-tracked — committed for a team, or synced across machines via a dotfiles/config repo — using/modelthe intended way (Enter in the picker, or/model <name>) writes themodelfield into that tracked file (per the v2.1.153 docs). So every routine model switch dirties a committed file: constantgit statusnoise, spurious diffs, and merge conflicts when the same file is touched on two machines. Model choice is inherently per-user and often per-machine; persisting it into shared, tracked state couples a personal preference to team/repo state.Suggestion: persist
/model(and/effort) saved preferences to the gitignoredsettings.local.json(Local scope) rather thansettings.json. Local already overrides Project and User in precedence, so the saved default still wins — without ever dirtying a tracked file. This dovetails with the "session-local by default + explicit save" asks above: whatever does get persisted should land in the Local scope.Workaround today (imperfect): a git
cleanfilter bound via.gitattributesthat strips themodelline from git's view ofsettings.json:This kills the churn —
git statusstays clean no matter what model the working tree holds — but it's per-clone setup, and any checkout that rewritessettings.jsonresets the on-disk model. That people reach for git filters to paper over a config write is itself a sign the persisted preference belongs in the gitignored Local scope.No, I don't think v2.1.211 fixes this. That item is scoped to subagent model inheritance (a subagent losing its explicit override and falling back to the parent's model on resume), which is a different path from
/modeland/effortwriting to globalsettings.json. Agreeing with @jstm88 that it's adjacent at best.What's more telling is that the changelog already contains fixes circling this exact bug without changing the scoping:
/modelin one of them". This is precisely the cross-session leak in this issue, and the fix was to correct the statusline display rather than to stop one session's/modelfrom reaching another's state./effortnow confirms when your chosen level will persist as the default for new sessions". That's the visibility mitigation, i.e. a warning attached to the behavior rather than a scope fix.effortLevelchanges insettings.jsonwhen forked through the daemon". This arguably tightens the coupling to global settings rather than loosening it.Three separate fixes have touched the symptoms; none introduced a session-scoped override. The v2.1.86 one is the clearest: when the response to "one session's
/modelleaked into another" is to fix the statusline rendering, the write itself is being treated as correct behavior to render accurately rather than as the bug.Confirming @kitaekatt's version-control observation first-hand, on v2.1.211: my
~/.claude/settings.jsonis symlinked from a tracked dotfiles repo, and it had nomodelfield at all while I was on the default, so this stayed invisible. Selecting a model once via/modelimmediately wrote"model": "claude-fable-5[1m]"into the tracked file, and reserialization rewrote an adjacent unrelated line as well, so a routine model switch yields a multi-hunk diff in a committed file.That said, I'd frame the fix differently. Persisting to
settings.local.jsoninstead would quiet the git noise, but it wouldn't address this issue: Local scope is still global across sessions, so one session's/modelwould go on changing every other session's default. It removes the diff, not the leakage. I tracksettings.jsondeliberately and am fine with a saved default living there.The missing concept seems to be scope, not location. There are two genuinely distinct things a user wants, and today only one exists:
/modelcurrently conflates them, so the only way to express (2) is to perform (1) and accept the side effects. A session-scoped default with an explicit opt-in to persist (/model --save, or a separate/model-default) would give both, and would make the fleet-view, cost, and version-control complaints in this thread fall out as consequences rather than needing individual patches.One data point on observed behavior, since the reports here differ slightly.
/modelpersists and applies to new sessions, but it does not appear to retroactively change sessions already open in other tabs. My tentative read is that other sessions pick up the change when they reload rather than while live, which would be consistent with @jstm88's session that switched to Haiku on reload. I haven't tested rigorously, so treat it as a hint: worth pinning down whether the leak happens at reload specifically, since that affects where a fix needs to sit.