Background `bg-spare` workers ignore project-level `env.ANTHROPIC_MODEL`, applying user-level value instead (cwd race during pre-warm)

Open 💬 1 comment Opened Jun 5, 2026 by MadSkittles

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 ensureFastPathSettingsLoadedKuH(), 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

  1. ~/.claude/settings.json:

``json
{ "env": { "ANTHROPIC_MODEL": "claude-opus-4-7" } }
``

  1. <project>/.claude/settings.json:

``json
{ "env": { "ANTHROPIC_MODEL": "claude-opus-4-7[1m]" } }
``

  1. pkill -f "claude daemon run" to clear any cached daemon/spare pool.
  2. From the project directory: claude agents
  3. In Agent View, dispatch a new agent.
  4. 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)

  1. Supervisor reads settings correctly. Supervisor inherits the launching process's env (including project .claude/settings.json's env block via KuH() resolved against the project cwd at startup). supervisor.process.env.ANTHROPIC_MODEL = claude-opus-4-7[1m]
  1. 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.
  1. Spare's startup re-loads settings against /tmp cwd. 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.

  1. Worker bootstrap chdir's into the project but does not re-apply settings env. _Y4 (worker bootstrap on claim) does process.chdir(project_cwd) then Object.assign(process.env, dispatch.env) — neither step re-resolves settings. The user-level value persists for the lifetime of the worker.
  1. qT4() dispatch propagation hides the issue further — for some code paths, providerEnv is captured via qT4() from the supervisor's process.env, but the in-worker KuH() 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-spare daemon doesn't invalidate spares whose cwd is deleted — adjacent bug, same /tmp/cc-daemon-*/spare/ pre-warm cwd surface.

Suggested Fix

One of:

  1. 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 /tmp staging cwd. Easiest: re-run KuH() in _Y4 after process.chdir(q) and before Object.assign(process.env, H.env).
  2. 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.
  3. Pre-warm spares with no cwd-resolution side effects. Move env injection from spare bootstrap to claim-time bootstrap (_Y4).

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗