[BUG] Agent tool `model` parameter silently overridden by CLAUDE_CODE_SUBAGENT_MODEL — no signal in tool_result or transcript
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
When CLAUDE_CODE_SUBAGENT_MODEL is set in the environment, the harness silently overrides the Agent tool's per-call model: parameter with the env-pinned value.
The override is invisible:
- Parent transcript records the requested model
- Child transcript shows the env-pinned model actually ran
tool_resultcontains noeffective_modelfield- No stderr warning fires
- No transcript event records the clamp
The parent agent has no programmatic way to detect that its routing intent was preempted. The Agent tool's narration ("spawning haiku worker") looks like fabrication when in fact the agent is being truthful and the harness is clamping below it.
This affects any workflow that depends on tiered model routing — multi-tier review/audit pipelines, cost-tier orchestration, agentic loops where a parent dispatches cheaper workers, and any skill that uses the Agent tool's model: parameter for tier-keyed economics.
Operator-policy precedence (env > per-call) may be defensible by design. The silent-override behavior is not.
What Should Happen?
Either:
- Honor the per-call
model:parameter (per-call > env), OR - Keep current precedence (env > per-call) but surface the override:
- Add
effective_modelfield totool_result - Add
clamped_byfield naming the env var - Optionally emit one-time stderr warning per session on first clamp
- At minimum, amend the Agent tool schema docstring to document that
CLAUDE_CODE_SUBAGENT_MODELenv var preempts the parameter
Option 2 is the minimal viable fix. It preserves operator-policy precedence and only changes the observability surface, so it's the lowest-friction path to ship.
Error Messages/Logs
No error messages are emitted. The silent nature of the override IS the bug — there is no stderr output, no transcript event, no log line, no warning of any kind. The only evidence of the clamp is in the child subagent JSONL on disk, which requires manual jq walking to detect.
Steps to Reproduce
Minimal reproduction from a clean shell:
# 1. Confirm baseline: no env pin
env | grep CLAUDE_CODE_SUBAGENT_MODEL
# (should be empty)
# 2. Set the pin
export CLAUDE_CODE_SUBAGENT_MODEL=claude-sonnet-4-6
# 3. Launch Claude Code with any parent model
claude --model 'claude-opus-4-7[1m]' --dangerously-skip-permissions
- Inside the session, prompt:
````
Use the Agent tool to spawn a subagent. Set the Agent tool's
model parameter to "haiku". The subagent's prompt should be:
just respond with "hello" and stop. Use subagent_type "general-purpose".
- Wait for completion, then
/exit.
- Walk the JSONL files in a separate shell to verify what actually ran:
PROJECT=$(ls -td ~/.claude/projects/*/ | head -1)
SESSION=$(ls -t "$PROJECT"*.jsonl | head -1 | xargs basename | sed 's/.jsonl$//')
# Parent's recorded request:
jq -r '.message.content[]? | select(.type == "tool_use" and .name == "Agent")
| "requested model = \(.input.model)"' \
"$PROJECT/$SESSION.jsonl"
# Output: requested model = haiku
# Child's effective execution model:
jq -r 'select(.message.model != null) | .message.model' \
"$PROJECT/$SESSION/subagents/agent-"*.jsonl | sort -u
# Output: claude-sonnet-4-6
The parent requested model: "haiku". The child ran on claude-sonnet-4-6. There is no signal in the parent transcript that this happened.
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.138 (Claude Code)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
Empirical evidence — controlled 5-test matrix
Same account, same machine, single afternoon. Differs only on env-pin presence and parent model class.
| # | Launch | Env pin set | Parent model | Requested | Effective child | Honored |
|---|--------|------------|--------------|-----------|-----------------|---------|
| 1 | wrapper sets pin | claude-sonnet-4-6 | claude-opus-4-7 | haiku | claude-sonnet-4-6 | ❌ |
| 2 | wrapper sets pin | claude-sonnet-4-6 | claude-sonnet-4-6 | haiku | claude-sonnet-4-6 | ❌ |
| 3 | wrapper sets pin | claude-sonnet-4-6 | claude-opus-4-7 | haiku | claude-sonnet-4-6 | ❌ |
| 4 | raw claude | unset | claude-opus-4-7 | haiku | claude-haiku-4-5-20251001 | ✅ |
| 5 | raw claude (/model sonnet) | unset | claude-sonnet-4-6 | haiku | claude-haiku-4-5-20251001 | ✅ |
With env pin: 3/3 silent clamp. Without env pin: 2/2 honored.
Bug is parent-model-agnostic and wrapper-agnostic — the env var alone is the trigger. Earlier sessions on the same account (different CC version) measured 22 additional clamps under env-pinned configurations on Sonnet 4.6 panelist parents. Total measured: 25 clamps + 2 controls.
Schema gap
The Agent tool's model parameter is documented at runtime as:
Optional model override for this agent. Takes precedence over the agent definition's model frontmatter. If omitted, uses the agent definition's model, or inherits from the parent.
The description does not mention CLAUDE_CODE_SUBAGENT_MODEL preemption. An agent reading this schema reasonably concludes the parameter works as documented.
Why this matters — failure mode taxonomy
- Cost-tier inflation — cheap-model request silently runs as more expensive model. Bills/quota inflate.
- Quality-tier inflation — fast/light request silently runs as smarter model. Distorts calibration.
- Latency-tier inflation — wall-clock per session balloons because cheap workers are running as expensive workers.
- Trust erosion — divergence between agent narration and runtime reality looks like agent fabrication. Users spend compute debugging "did the agent lie?" before realizing the harness is clamping below it.
- Skill portability collapse — skills relying on tiered Agent dispatch cannot be portably evaluated by users with different env configs.
- Test/prod divergence — Claude Agent SDK consumers test against one env, deploy under another, observe different agent behaviors with no diagnostic.
- Regression invisibility — any future change to precedence rules silently shifts behavior with no upgrade-note signal.
Suggested fix (minimal — preserves precedence)
Add effective_model and clamped_by fields to the Agent tool's tool_result:
{
"type": "tool_result",
"tool_use_id": "...",
"content": "...",
"effective_model": "claude-sonnet-4-6",
"clamped_by": "CLAUDE_CODE_SUBAGENT_MODEL"
}
Optionally emit a one-time stderr line on the first clamp per session.
This preserves env-policy precedence and changes only the observability surface.
Suggested fix (documentation)
Amend Agent tool model parameter description to:
Optional model override for this agent. Takes precedence over the agent definition's model frontmatter, but may itself be preempted by theCLAUDE_CODE_SUBAGENT_MODELenvironment variable when set. Checkeffective_modelin the tool_result if you need to confirm what actually ran.
Related existing issues (cross-reference)
This filing isolates a variable that prior issues reported but couldn't pin down. Cross-references for triage clustering:
- #43869 "Subagent model routing is broken — all mechanisms resolve to parent model" — tested 5 mechanisms including Agent(model:) param, all silently ignored. Their evidence couldn't isolate which mechanism is the trigger. My controlled experiment isolates it: env var preempts the per-call param. When env is unset (rows 4 and 5 above), the per-call param works correctly.
- #52681 "Subagent
model:frontmatter pin silently ignored" — about frontmatter being ignored. Their stated workaround "Agent tool model: at invocation does work" is true only when env is unset. With env pin set, the workaround also breaks (rows 1-3 above).
- #54430 "Team agent --model ignores ANTHROPIC_DEFAULT_*_MODEL and CLAUDE_CODE_SUBAGENT_MODEL env vars" — different layer (CLI flag overriding env on team-agent spawn). Opposite precedence direction.
- #47350 "context: fork and subagents appear to use a different (lighter) model despite model configuration" — about behavioral degradation despite JSONL claiming correct model. Different shape (their JSONL says correct model but behavior contradicts; mine has JSONL explicitly showing wrong model).
The pattern across these issues is "model spec silently ignored at some layer." This filing is the controlled experiment that isolates env-pin-overrides-Agent-tool-param specifically.
Design space (discussion, NOT a fix request)
Whether env > per-call is the correct precedence is a design question worth its own thread:
- A (current): env > per-call, silent. Operator-policy wins absolutely.
- B: env > per-call, but annotated. (this issue's primary suggestion)
- C: per-call > env, env becomes default. Matches most CLI tools.
- D: env as cap — env can only clamp DOWN (cheaper), per-call can request cheaper. Operator caps ceiling, agents can opt-cheaper.
- E: split env into
_DEFAULT(default) and_OVERRIDE(always wins). Operator picks semantics.
I'm not requesting B/C/D/E in this issue — minimal fix is annotation under current precedence (Option B).
What I am NOT requesting
- Removal of the env var
- Changes to legitimate operator clamp semantics
- Any breaking change to existing behavior
The current precedence is defensible as operator policy. The bug is that it's invisible.
Privacy note
Happy to provide redacted parent + child JSONL excerpts privately if useful for the maintainer. The minimal repro above should be sufficient to reproduce on any clean account.
Filed in the spirit of helping the next operator avoid this rabbit hole. The product is good. This specific observability gap, once closed, will be one less thing for new heavy users to trip over.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗