Feature Request: SubagentStart hook should support updatedInput to enable deterministic model routing

Status Closed — not planned
Reported on v2.1.183
Maintainer reply None cached
Activity 6 comments · opened Jun 19, 2026 · closed Aug 4, 2026

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

View original on GitHub ↗

6 Comments

github-actions[bot] · 3 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/39814
  2. https://github.com/anthropics/claude-code/issues/44412
  3. https://github.com/anthropics/claude-code/issues/23901

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

Josef-Le · 3 months ago

Responding to the duplicate suggestions — these are related but not the same request:

#39814 and #44412 report that PreToolUse(matcher: "Agent") silently ignores updatedInput — 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 as not_planned (stale), not fixed.

#23901 was filed about SubagentStart supporting updatedPrompt for prompt injection, and it was closed as completed. We verified on 2.1.183 that additionalContext is supported via SubagentStart. However, model selection via updatedInput.model is a distinct capability — it was not part of #23901's scope, and it is still not implemented. Our testing confirms:

  • SubagentStart fires ✅
  • updatedInput.model returned 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.

Necmttn · 3 months ago

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._

Josef-Le · 3 months ago

@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 SubagentStart supports updatedInput.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_model per leg, hook_decision_id) would surface that. The minimal version that would close the gap for us:

  • SubagentStart input includes requested_model (so the hook has enough info to decide)
  • SubagentStart input also fires on continuation/compaction legs (or a new SubagentResume event)
  • The hook's updatedInput.model is applied to the effective model for that leg

Without 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).

github-actions[bot] · 2 months ago

Closing for now — inactive for too long. Please open a new issue if this is still relevant.

tiagovilasboas · 24 days ago

This is solved today — PreToolUse with matcher: "Task" works

Posting because this issue was closed as stale but the problem is real and there is a working solution.

The key finding: PreToolUse with matcher: "Task" does fire for subagent spawns and does honor updatedInput — the subagent starts with the rewritten model. The author tested matcher: "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:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Task",
        "hooks": [
          { "type": "command", "command": "downshift claude-code" }
        ]
      }
    ]
  }
}

The hook receives tool_input.prompt (the subagent's task), classifies it deterministically without any LLM call, and returns updatedInput with 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

  • Single binary, zero deps, installs in one line
  • Version-agnostic: claude-opus-4-9 matches the opus tier automatically via family prefix
  • Also handles Codex (spawn_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 from tool_input and 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