Each Claude subagent's model and effort in the native agent list, beside its task

Each Claude subagent's model and effort in Claude Code's agent list.

Claude Code's agent list shows a subagent's task but not what it runs on. With this mod each row carries it, the way the codex mod's rows do:
Review auth · Opus 5.5 (high)
Check label.ts · Sonnet 5.5 (medium)
Say ok · Haiku
| The agent | Its label |
|---|---|
general-purpose, a fork, an agent file's inherit | the parent's model and effort |
an agent file with model: (and effort:) | that model, and that effort |
the Agent tool's model option | that model; the parent's effort only if the same model |
| Explore, Plan and other built-ins | none: their model is the engine's to choose |
a plugin's agent (codex:read) | none: the plugin labels its own |
Where the model cannot be known before the agent starts, the row is left as it was rather than guessed.
/plugin marketplace add alex2481kobe/claude-mods
/plugin install agent-models@claude-mods
agent.spawn hook, which runs before the subagent starts, appends · <model> (<effort>) to the task's description; the agent list shows that description.model option, else the agent file's, else the parent's for the agents that inherit it. Agent files are read from the project's .claude/agents/ and your own (~/.claude/agents/, or $CLAUDE_CONFIG_DIR/agents/), matched by their name.effort, else the effort of the parent's latest request, and that only when the agent runs on the parent's own model: another model may take no effort at all (Haiku takes none).claude-opus-5-5 is Opus 5.5.agents/, or one defined in settings, a flag or policy, is not read, so its agent goes unlabelled.model: line is left unlabelled.haiku is Haiku.effort is shown even when the Agent tool's model option overrides its model.claude -p).claude plugin validate plugins/agent-models
claude plugin test plugins/agent-models
claude --plugin-dir plugins/agent-modelshooks/register.ts 62 lines1import type { AgentSpawnInput, EngineInterface, Register } from 'claude-code'
2
3import { definitionOf, labelOf, type Definition } from './label'
4
5// Shows each Claude subagent's model and effort in the native agent list:
6// ` · Opus 5.5 (high)` after its task, as the codex mod does for Codex.
7
8// The effort of each loop's latest request: '' is the main loop.
9const efforts = new Map<string, string>()
10
11// Where agent files live: the project's own first, then the person's. A
12// definition from anywhere else (settings, a flag, policy) is not read, and
13// its agent goes unlabelled.
14async function agentDirs($: EngineInterface, provider: string): Promise<string[]> {
15 const home = await $.env.get('HOME')
16 const config = (await $.env.get('CLAUDE_CONFIG_DIR')) || (home && `${home}/.claude`)
17 const user = config ? [`${config}/agents`] : []
18 return provider === 'user' ? user : [`${await $.session.root()}/.claude/agents`, ...user]
19}
20
21async function definitionNamed($: EngineInterface, type: string, provider: string): Promise<Definition | undefined> {
22 for (const dir of await agentDirs($, provider)) {
23 const files = await $.fs.list(dir).catch(() => [])
24 for (const file of files) {
25 if (file.kind !== 'file' || !file.name.endsWith('.md')) continue
26 const text = await $.fs.read(`${dir}/${file.name}`).catch(() => '')
27 const definition = definitionOf(text, file.name)
28 if (definition?.name === type) return definition
29 }
30 }
31 return undefined
32}
33
34export const register: Register = on => {
35 on('turn.step', async function* ($, e, next) {
36 if (e.effort === undefined) efforts.delete(e.agentId ?? '')
37 else efforts.set(e.agentId ?? '', String(e.effort))
38 return yield* next(e)
39 })
40
41 on('agent.spawn', async ($, e, next) => {
42 // The label is decoration: an agent starts whether or not it can be made.
43 const label = await labelFor($, e).catch(() => undefined)
44 return next(label ? { ...e, description: `${e.description} · ${label}` } : e)
45 })
46}
47
48async function labelFor($: EngineInterface, e: AgentSpawnInput): Promise<string | undefined> {
49 if (e.subagentType.includes(':')) return undefined
50 const isBuiltIn = e.provider.plugin === 'engine'
51 const definition = e.fork || isBuiltIn ? undefined : await definitionNamed($, e.subagentType, e.provider.plugin)
52 const spawn = {
53 type: e.subagentType,
54 model: e.model,
55 fork: e.fork,
56 isBuiltIn,
57 parentModel: e.parentModel,
58 parentEffort: efforts.get(e.parentAgentId ?? ''),
59 }
60 return labelOf(spawn, definition)
61}
62hooks/label.ts 59 lines1// What a subagent runs on, worked out before it starts: the agent list shows
2// the spawn's description, and only a rewrite made before the spawn shows.
3
4// An agent file's frontmatter, as far as the label needs it.
5export type Definition = { name: string; model?: string; effort?: string }
6
7export type Spawn = {
8 type: string
9 // The Agent tool's `model` parameter, when given.
10 model?: string
11 fork: boolean
12 isBuiltIn: boolean
13 parentModel: string
14 // The effort of the parent's latest request, when its model has one.
15 parentEffort?: string
16}
17
18// Built-in types that run on the parent's model and effort.
19const INHERITS = new Set(['general-purpose'])
20
21// The frontmatter of an agent file: its `name` (else the file's name), and
22// `model` and `effort` when set. A file without frontmatter is no agent.
23export function definitionOf(text: string, fileName: string): Definition | undefined {
24 const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text)
25 if (!match) return undefined
26 const fields: Record<string, string> = {}
27 for (const line of match[1]!.split(/\r?\n/)) {
28 const field = /^([A-Za-z]+):\s*(.*?)\s*$/.exec(line)
29 if (field) fields[field[1]!] = field[2]!.replace(/^(["'])(.*)\1$/, '$2')
30 }
31 const name = fields.name || fileName.replace(/\.md$/, '')
32 return { name, model: fields.model || undefined, effort: fields.effort || undefined }
33}
34
35// Claude model ids as people say them: `claude-opus-5-5` reads `Opus 5.5`,
36// a dated id like `claude-haiku-4-5-20251001` reads `Haiku 4.5`, an alias
37// like `haiku` reads `Haiku`. Anything else is shown as it is.
38export function nameOf(id: string): string {
39 const match = /^claude-([a-z]+)-(\d+)-(\d+)(?:-\d{8})?(\[1m\])?$/i.exec(id)
40 const word = /^[a-z]+$/.test(id) ? id : match?.[1]
41 if (!word) return id
42 const name = `${word[0]!.toUpperCase()}${word.slice(1)}`
43 return match ? `${name} ${match[2]}.${match[3]}` : name
44}
45
46// The label for the agent list, e.g. `Opus 5.5 (high)`, or undefined when
47// the model cannot be known here. Plugin types (`codex:read`) label
48// themselves. Effort shows only where it is known to carry over: the
49// definition sets it, or the agent runs on the parent's own model.
50export function labelOf(spawn: Spawn, definition?: Definition): string | undefined {
51 if (spawn.type.includes(':')) return undefined
52 const chosen = spawn.fork ? undefined : (spawn.model ?? definition?.model)
53 const inherits = spawn.fork || chosen === 'inherit' || (!chosen && spawn.isBuiltIn && INHERITS.has(spawn.type))
54 const model = inherits ? spawn.parentModel : chosen
55 if (!model) return undefined
56 const effort = definition?.effort ?? (model === spawn.parentModel ? spawn.parentEffort : undefined)
57 return effort ? `${nameOf(model)} (${effort})` : nameOf(model)
58}
59