[BUG] Agent tool `model` parameter silently overridden by CLAUDE_CODE_SUBAGENT_MODEL — no signal in tool_result or transcript

Resolved 💬 1 comment Opened May 10, 2026 by aharoJ Closed Jun 8, 2026

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_result contains no effective_model field
  • 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:

  1. Honor the per-call model: parameter (per-call > env), OR
  2. Keep current precedence (env > per-call) but surface the override:
  • Add effective_model field to tool_result
  • Add clamped_by field naming the env var
  • Optionally emit one-time stderr warning per session on first clamp
  1. At minimum, amend the Agent tool schema docstring to document that CLAUDE_CODE_SUBAGENT_MODEL env 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
  1. 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".
``

  1. Wait for completion, then /exit.
  1. 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

  1. Cost-tier inflation — cheap-model request silently runs as more expensive model. Bills/quota inflate.
  2. Quality-tier inflation — fast/light request silently runs as smarter model. Distorts calibration.
  3. Latency-tier inflation — wall-clock per session balloons because cheap workers are running as expensive workers.
  4. 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.
  5. Skill portability collapse — skills relying on tiered Agent dispatch cannot be portably evaluated by users with different env configs.
  6. Test/prod divergence — Claude Agent SDK consumers test against one env, deploy under another, observe different agent behaviors with no diagnostic.
  7. 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 the CLAUDE_CODE_SUBAGENT_MODEL environment variable when set. Check effective_model in 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.

View original on GitHub ↗

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