Feature Request: SubagentStart hook should support updatedInput to enable deterministic model routing
Problem
There is currently no deterministic way to enforce model routing for subagents spawned via the Agent() tool. The only available mechanisms are advisory (rely on the orchestrating model choosing the right model: param), or blunt (a single CLAUDE_CODE_SUBAGENT_MODEL env var that forces all agents to one model regardless of task).
What was tried and why it doesn't work
I spent significant time empirically testing every available hook and mechanism on CC 2.1.183:
| Mechanism | Fires for Agent? | Can modify model? |
|---|---|---|
| PreToolUse(matcher: "Agent") | ❌ Does not fire | — |
| SubagentStart | ✅ Fires | ❌ updatedInput silently ignored |
| PermissionRequest(matcher: "Agent") | ❌ Does not fire | — |
| permissions.deny | ✅ Fires | ❌ Can block, not reroute |
| CLAUDE_CODE_SUBAGENT_MODEL env var | ✅ (overrides everything) | Blunt — no task awareness |
SubagentStart fires correctly and receives the session_id, transcript_path, cwd, agent_id, and agent_type. However:
- The hook input does not include the requested model
- Returning
{"hookSpecificOutput": {"updatedInput": {"model": "claude-haiku-4-5-20251001"}}}is silently ignored — subagent runs with whatever model was originally requested - Returning
{"decision": "block"}or exiting with code 2 is also silently ignored — subagent runs anyway - Returning
{"hookSpecificOutput": {"additionalContext": "..."}}is not injected into the subagent
CLAUDE_CODE_SUBAGENT_MODEL does work as a full override (tested empirically: setting it in the process environment forces haiku even when model: "sonnet" is explicitly specified in the Agent call). But it's a session-wide blunt instrument — it prevents using Sonnet agents for legitimate parallel judgment tasks.
Requested behavior
Either (or both) of the following would solve the problem:
Option A: SubagentStart hook supports updatedInput.model
When a SubagentStart hook returns updatedInput, CC should apply the modified model field before initializing the subagent context. The hook should also receive the requested model in its input so hooks can make informed decisions:
{
"session_id": "...",
"agent_id": "...",
"agent_type": "general-purpose",
"model": "claude-sonnet-4-6", // ← add this
"task_description": "...", // ← add this (first user message or task prompt)
"hook_event_name": "SubagentStart"
}
Return value to override the model:
{
"hookSpecificOutput": {
"updatedInput": {
"model": "claude-haiku-4-5-20251001"
}
}
}
Option B: PreToolUse fires for Agent tool calls
Allow PreToolUse hooks with matcher: "Agent" to intercept Agent spawns, with the ability to modify tool_input.model via updatedInput — consistent with how PreToolUse works for all other tools.
Use case
This enables building a deterministic model router: a small classifier (keyword heuristic or local 3B model) that runs in a hook script, classifies the task as mechanical (→ haiku) or judgment-based (→ sonnet), and rewrites the model accordingly. The LLM orchestrator does not need to be involved in the routing decision at all.
Current workaround: all routing is advisory — the mandate is injected via UserPromptSubmit hook but the orchestrating model still decides whether to follow it. This produces inconsistent behavior in practice.
Environment
- Claude Code: 2.1.183
- Platform: macOS (arm64)
- Testing methodology: empirical hook wiring + subagent spawning, not inference from docs
6 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Responding to the duplicate suggestions — these are related but not the same request:
#39814 and #44412 report that
PreToolUse(matcher: "Agent")silently ignoresupdatedInput— that's true and still reproducible on 2.1.183. But both were filed about prompt modification, not model selection. Our Option B covers the same surface area (PreToolUse for Agent), and we agree it's broken for prompt modification too. However, #39814 was closed asnot_planned(stale), not fixed.#23901 was filed about
SubagentStartsupportingupdatedPromptfor prompt injection, and it was closed ascompleted. We verified on 2.1.183 thatadditionalContextis supported via SubagentStart. However, model selection viaupdatedInput.modelis a distinct capability — it was not part of #23901's scope, and it is still not implemented. Our testing confirms:SubagentStartfires ✅updatedInput.modelreturned by the hook → silently ignored ❌decision: block→ silently ignored ❌additionalContext→ injected ✅ (this was #23901's scope)The specific gap this FR addresses — deterministic model routing from a hook script, without LLM involvement — is not covered by any of those issues. The model field is absent from the SubagentStart hook input entirely, and neither hook type (PreToolUse nor SubagentStart) can currently modify which model a subagent runs on.
If routing becomes hook-modifiable, I would emit both requested and effective model on the child run.
Useful receipt fields: parent session id, agent id/type, requested model, hook/routing decision id, effective child model, and whether later continuation/compaction legs changed model. That makes deterministic routing auditable and catches the common failure mode where the first dispatch obeys the route but a continuation silently drifts.
---
_Generated with ax._
@Necmttn — agreed, and the continuation/compaction drift point is the part I'd most want surfaced.
The core FR is about enforcement (can a hook override which model spawns), but your observability angle is orthogonal and equally important: even if Option A lands and
SubagentStartsupportsupdatedInput.model, a compaction leg creates a new session context that may reinitialize with the parent session's default model rather than the hook-enforced one. The hook fires on the initial spawn, not on subsequent legs — so the override can silently lapse mid-task.The fields you listed (
requested_model,effective_modelper leg,hook_decision_id) would surface that. The minimal version that would close the gap for us:SubagentStartinput includesrequested_model(so the hook has enough info to decide)SubagentStartinput also fires on continuation/compaction legs (or a newSubagentResumeevent)updatedInput.modelis applied to the effective model for that legWithout the per-leg coverage, a router that works on first dispatch fails silently when the session compacts. That's the worst failure mode — you think haiku ran the whole task, it ran the first leg haiku and the rest sonnet (or the reverse).
Closing for now — inactive for too long. Please open a new issue if this is still relevant.
This is solved today — PreToolUse with
matcher: "Task"worksPosting because this issue was closed as stale but the problem is real and there is a working solution.
The key finding:
PreToolUsewithmatcher: "Task"does fire for subagent spawns and does honorupdatedInput— the subagent starts with the rewritten model. The author testedmatcher: "Agent"(which does not fire) but not"Task"(which does). Both tool names exist depending on the CC version and context.Minimal hook config that routes today:
The hook receives
tool_input.prompt(the subagent's task), classifies it deterministically without any LLM call, and returnsupdatedInputwith the right model. Trivial work → haiku, complex → opus. This has been running reliably in practice.I built a Go binary that implements exactly this — deterministic classification + model routing for Claude Code, Cursor, and Codex via their respective PreToolUse hooks:
harness-downshift → https://github.com/tiagovilasboas/harness-downshift
claude-opus-4-9matches the opus tier automatically via family prefixspawn_agent), Cursor, Grok, and OpenRouter-prefixed model IDs transparently---
That said, Option A from the original request (SubagentStart supporting
updatedInput.model+ exposing the requested model and task description in the hook payload) would still be a cleaner solution. The current PreToolUse path works but requires reading the prompt fromtool_inputand is one CC update away from breaking if the tool name changes. SubagentStart with the model exposed would make this officially supported.— Tiago de Carvalho Vilas Boas, author of harness-downshift