Background `bg-spare` workers ignore project-level `env.ANTHROPIC_MODEL`, applying user-level value instead (cwd race during pre-warm)
Background bg-spare workers ignore project-level env.ANTHROPIC_MODEL, applying user-level value instead (cwd race during pre-warm)
Summary
When claude agents (Agent View) dispatches a new background worker, the worker is served from a pre-warmed --bg-spare pool. These spare workers are spawned with cwd = /private/tmp/cc-daemon-*/<id>/spare, not the project directory. During startup the spare loads settings via ensureFastPathSettingsLoaded → KuH(), which resolves project settings relative to that /tmp cwd — so projectSettings is effectively empty, and ~/.claude/settings.json (user-level) becomes the only source for env.* keys in usH (e.g. ANTHROPIC_MODEL).
Result: project-level .claude/settings.json env.ANTHROPIC_MODEL is silently overridden by the user-level value for every agent dispatched from Agent View, even though the supervisor process itself reads project settings correctly.
Documentation states project settings override user settings; for env.ANTHROPIC_MODEL in claude agents this is reversed.
Environment
- Claude Code v2.1.163 (native install,
/Users/<user>/.local/share/claude/versions/2.1.163) - macOS Darwin 25.5.0 (Apple Silicon)
- Shell: zsh, launched from ghostty terminal (reproduced under cmux's wrapped claude as well — not wrapper-related)
bgIsolation: "none"set (worktree isolation off)
Repro
~/.claude/settings.json:
``json``
{ "env": { "ANTHROPIC_MODEL": "claude-opus-4-7" } }
<project>/.claude/settings.json:
``json``
{ "env": { "ANTHROPIC_MODEL": "claude-opus-4-7[1m]" } }
pkill -f "claude daemon run"to clear any cached daemon/spare pool.- From the project directory:
claude agents - In Agent View, dispatch a new agent.
- Inside the new agent, ask "what model are you using" — or inspect the worker's env with
ps -E -p <worker-pid>.
Expected: ANTHROPIC_MODEL=claude-opus-4-7[1m] (project value wins per documented precedence).
Actual: ANTHROPIC_MODEL=claude-opus-4-7 (user-level value).
Confirmed via direct env inspection:
$ ps -E -p <supervisor-pid> | grep ANTHROPIC_MODEL
ANTHROPIC_MODEL=claude-opus-4-7[1m] # supervisor reads project settings correctly
$ ps -E -p <unclaimed-spare-pid> | grep ANTHROPIC_MODEL
ANTHROPIC_MODEL=claude-opus-4-7 # spare wrote user-level value into its own env
$ ps -E -p <claimed-worker-pid> | grep ANTHROPIC_MODEL
ANTHROPIC_MODEL=claude-opus-4-7 # claimed worker inherits spare's env, never re-resolves
$ lsof -p <unclaimed-spare-pid> | grep cwd
... cwd ... /private/tmp/cc-daemon-501/<daemon-id>/spare
Diagnosis (traced through 2.1.163 binary)
- Supervisor reads settings correctly. Supervisor inherits the launching process's env (including project
.claude/settings.json'senvblock viaKuH()resolved against the project cwd at startup).supervisor.process.env.ANTHROPIC_MODEL = claude-opus-4-7[1m]✅
- Supervisor spawns spare with cloned env.
$TT()constructs spare env:{ ...process.env, CLAUDE_CODE_SESSION_KIND: "bg", ... }. The spare initially has the correct[1m]value.
- Spare's startup re-loads settings against
/tmpcwd. The CLI entry handler:
``js``
if (H[0]==="--bg-spare") {
let { ensureFastPathSettingsLoaded: j } = await Promise.resolve().then(...);
await j(); // <-- triggers KuH()
let { runBgSpare: J } = await Promise.resolve().then(...);
await J(H.slice(1));
return;
}
KuH() final loop:
``js``
for (let H of Ek()) { // Ek = [userSettings, projectSettings, localSettings, flagSettings, policySettings]
let _ = JK_(x6(H)?.env, H);
for (let [q, K] of Object.entries(_))
if (usH.has(q.toUpperCase()))
process.env[q] = K; // <-- direct assignment, overwrites inherited value
}
With cwd = /private/tmp/cc-daemon-*/<id>/spare, x6("projectSettings") returns null (no .claude/settings.json along that path). Only userSettings contributes to the loop → process.env.ANTHROPIC_MODEL is replaced with the user-level value, overwriting the project value that was correctly inherited from the supervisor.
- Worker bootstrap chdir's into the project but does not re-apply settings env.
_Y4(worker bootstrap on claim) doesprocess.chdir(project_cwd)thenObject.assign(process.env, dispatch.env)— neither step re-resolves settings. The user-level value persists for the lifetime of the worker.
qT4()dispatch propagation hides the issue further — for some code paths,providerEnvis captured viaqT4()from the supervisor'sprocess.env, but the in-workerKuH()re-run still overwrites with the (incorrect) user-level snapshot.
Why "wrong" instead of "missing"
The bug is not that KuH() is buggy — it's that KuH() runs in the spare's pre-warm cwd, which by design is /tmp/cc-daemon-*/spare/, far away from any project. Documented precedence (project > user) is invariant-violated because projectSettings is unreachable from the cwd KuH was called in.
Workaround
Mirror the desired env.ANTHROPIC_MODEL value into ~/.claude/settings.json so that user-level and project-level happen to agree. This is brittle (one-machine, breaks for multi-project users with per-project model preferences).
Related Issues
- #59112 — Background sessions don't honor user
permissions.defaultMode: bypassPermissions— same dispatch-path-doesn't-re-resolve-settings family. - #38859 — Background agents cannot get Bash permissions — same family.
- #61740 —
bg-sparedaemon doesn't invalidate spares whose cwd is deleted — adjacent bug, same/tmp/cc-daemon-*/spare/pre-warm cwd surface.
Suggested Fix
One of:
- Defer
KuH()until after the worker chdir's into the project cwd. Spare pre-warm should load settings against the target cwd, not its own/tmpstaging cwd. Easiest: re-runKuH()in_Y4afterprocess.chdir(q)and beforeObject.assign(process.env, H.env). - Don't overwrite inherited env values for
usH-listed keys when the source settings file is missing entirely — only treat presence-of-value as authoritative. - Pre-warm spares with no cwd-resolution side effects. Move env injection from spare bootstrap to claim-time bootstrap (
_Y4).
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗