Runs the main conversation on Haiku as a dispatcher that never does work itself, and routes every task to a minion on the model tier that fits it: Haiku…

A Claude Code mod that turns the main conversation into a dispatcher on a small model, one that never does the work itself, and sends every task to a minion on the model tier that fits it.
At the prompt of a terminal session:
/plugin install minions --marketplace ericfisherdev/ericfisherdev-plugins
Answer y to add the marketplace and pick a scope. Requires Claude Code 2.1.296 or later (function hooks).
turn.step), whatever /model says. Minions keep their own models. The status line shows minions: haiku.| Minion | Model | Effort | For |
|---|---|---|---|
minions:scout | Haiku | medium | Simple fetch, read and scan tasks with checkable answers |
minions:mapper | Sonnet | medium | Basic, well-scoped code mapping and analysis; scans that need judgment |
minions:planner | Opus | medium | Planning, dependency analysis, organization work; writes the plan a coder follows |
minions:coder | Sonnet | medium | Implements a written plan, then verifies it with a check that exercises it |
minions:analyst | Opus | xhigh | Intense code analysis within one sitting: root causes, tricky bugs, architecture, security review |
minions:researcher | Fable | high | Deep research, whole-repository analysis, work larger than one sitting, or an analyst that fell short |
Only the coder holds the file-writing tools. The scout has no shell at all and runs without CLAUDE.md to stay cheap. The mapper, planner, analyst and researcher keep Bash for read-only commands (git log, a test run), so for them read-only is enforced by their instructions, not by their tool list.
model (haiku, sonnet, opus or fable). One that names none is refused, so no agent ever runs on a tier nobody chose. Forks, resumes, workflow agents and spawns made inside a minion are never touched.The table was checked against Anthropic's model guidance (the models overview, the choosing-a-model and cost pages, the Claude Code sub-agent and model-config docs, and the Haiku 5.5, Sonnet 5.5, Opus 5.5 and Fable 5.1 announcements) before it was written:
opusplan alias.| Command | Description |
|---|---|
/minions | The routing table, the dispatcher model and the guard state |
In /config › minions:
orchestratorModel (default haiku): an alias (haiku, sonnet, opus, fable) or a full model id for the main conversation. An alias resolves to the current generation's id (claude-haiku-5-5 and so on); pass a full id on Bedrock, Vertex or Foundry.guard (default on): refuse work tools to the main conversation. Off, the routing rules are advice only.Explore, Plan, general-purpose) still exist; the dispatcher must pass model to use one.claude --plugin-dir plugins/minions, claude plugin validate plugins/minions, claude plugin test plugins/minions.hooks/register.ts 105 lines1// minions: the main conversation is a dispatcher on a small model that never
2// does the work itself. Every task goes to a minion on the model tier that
3// fits it (see roster.ts), and the guards (guard.ts) refuse the dispatcher the
4// tools that do work and refuse any spawn that names no model.
5
6import type { Register, PromptComposeSection } from 'claude-code'
7import { MINIONS, PLUGIN, ROUTING_TABLE, minionType } from './roster'
8import { spawnDenial, toolCallDenial } from './guard'
9import { resolveModelId } from './models'
10
11const str = (v: unknown): string | undefined => (typeof v === 'string' && v !== '' ? v : undefined)
12
13/** The section the dispatcher reads at the end of its system prompt. */
14export const dispatcherSection = (model: string, isGuarded: boolean): PromptComposeSection => ({
15 id: `${PLUGIN}:dispatcher`,
16 scope: 'session',
17 text: `# minions
18
19This applies to the main conversation only. Inside a subagent, ignore this section and do the task you were given.
20
21You are the dispatcher, running on ${model}. You NEVER do the work yourself: you do not read, search, edit, run commands or fetch pages. ${isGuarded ? 'Those tools are refused to you here.' : 'Treat those tools as off limits.'} For every task the user gives you, spawn a minion with the Agent tool (the \`subagent_type\` names the minion, its model is fixed), give it the whole task in its prompt, wait for its report, and relay what matters. Spawn independent minions in one message so they run in parallel.
22
23Routing table:
24${ROUTING_TABLE}
25
26Any other agent type you spawn must name its \`model\` explicitly; a spawn that names none is refused.`,
27})
28
29/** The text `/minions` prints. */
30export const statusText = (model: string, isGuarded: boolean): string =>
31 [
32 `minions: dispatcher on ${model}; guard ${isGuarded ? 'on' : 'off'} (/config › minions).`,
33 '',
34 'Routing table:',
35 ROUTING_TABLE,
36 '',
37 'Minions:',
38 ...MINIONS.map(m => `- ${minionType(m)}: ${m.model} at ${m.effort} effort`),
39 ].join('\n')
40
41export const register: Register = (on, options) => {
42 const orchestratorModel = str(options['orchestratorModel']) ?? 'haiku'
43 const orchestratorModelId = resolveModelId(orchestratorModel)
44 const isGuarded = options['guard'] !== false
45
46 on('session.start', async ($, e, next) => {
47 for (const minion of MINIONS) {
48 await $.agent.register({
49 name: minion.name,
50 description: minion.description,
51 prompt: minion.prompt,
52 model: minion.model,
53 effort: minion.effort,
54 ...(minion.tools === undefined ? {} : { tools: minion.tools }),
55 ...(minion.disallowedTools === undefined ? {} : { disallowedTools: minion.disallowedTools }),
56 ...(minion.omitClaudeMd === undefined ? {} : { omitClaudeMd: minion.omitClaudeMd }),
57 })
58 }
59 await $.command.register({
60 name: PLUGIN,
61 description: 'The minions routing table, the dispatcher model and the guard state.',
62 })
63 $.ui.status(`minions: ${orchestratorModel}`)
64
65 return next(e)
66 })
67
68 on('command.run', { command: PLUGIN }, () => ({ text: statusText(orchestratorModel, isGuarded) }))
69
70 // The main conversation's every request goes to the dispatcher model; a
71 // minion's requests keep its own.
72 on('turn.step', async function* ($, e, next) {
73 if (e.agentId !== undefined) return yield* next(e)
74
75 return yield* next({ ...e, model: orchestratorModelId })
76 })
77
78 on('prompt.compose', async ($, e, next) => {
79 const { sections } = await next(e)
80
81 return { sections: [...sections, dispatcherSection(orchestratorModel, isGuarded)] }
82 })
83
84 if (isGuarded) {
85 on('tool.call', ($, e, next) => {
86 const deny = toolCallDenial({ tool: e.tool, agentId: e.agentId, origin: next.origin })
87
88 return deny === undefined ? next(e) : { deny }
89 }).catch(($, e, next) => (next.called ? next(e) : { deny: `${PLUGIN}: its guard failed.` }))
90 }
91
92 on('agent.spawn', ($, e, next) => {
93 const deny = spawnDenial({
94 subagentType: e.subagentType,
95 model: e.model,
96 parentAgentId: e.parentAgentId,
97 fork: e.fork,
98 isResume: e.resume !== undefined,
99 isWorkflow: e.workflow !== undefined,
100 })
101
102 return deny === undefined ? next(e) : { deny }
103 }).catch(($, e, next) => (next.called ? next(e) : { deny: `${PLUGIN}: its spawn guard failed.` }))
104}
105hooks/roster.ts 116 lines1// The roster: every minion type the mod registers, each pinned to one model
2// tier, and the routing table the dispatcher reads. One reason to change:
3// which tier does which kind of work.
4
5/** One minion: an agent definition the engine registers as `minions:<name>`. */
6export type Minion = {
7 name: string
8 model: 'haiku' | 'sonnet' | 'opus' | 'fable'
9 effort: 'low' | 'medium' | 'high' | 'xhigh' | 'max'
10 description: string
11 prompt: string
12 tools?: readonly string[]
13 disallowedTools?: readonly string[]
14 omitClaudeMd?: true
15}
16
17// The context7 tools under both names the engine gives them: a server the
18// user configured, and the one the context7 plugin ships.
19const CONTEXT7_TOOLS = [
20 'mcp__context7__resolve-library-id',
21 'mcp__context7__query-docs',
22 'mcp__plugin_context7_context7__resolve-library-id',
23 'mcp__plugin_context7_context7__query-docs',
24] as const
25
26/** The tools that only read: files, search, the web and documentation. */
27const READ_TOOLS = ['Read', 'Grep', 'Glob', 'WebFetch', 'WebSearch', 'ToolSearch', ...CONTEXT7_TOOLS] as const
28
29/**
30 * The read tools plus a shell for read-only commands (git log, a test run).
31 * A shell can write, so a minion holding it is read-only by its instructions,
32 * not by its tool list.
33 */
34const READ_AND_SHELL_TOOLS = [...READ_TOOLS, 'Bash'] as const
35
36const REPORT_FORMAT = `Report with the answer first, then the evidence as file:line references. Say plainly what you could not find or verify. Do not pad.`
37
38const NO_WRITES = `You are read-only: never edit, write or create files, and never run a shell command that changes state.`
39
40export const MINIONS: readonly Minion[] = [
41 {
42 name: 'scout',
43 model: 'haiku',
44 effort: 'medium',
45 description:
46 'Simple fetch, read and scan tasks with checkable answers: find where X is defined, list callers of Y, read a file or URL and report facts, grep a pattern. Not for anything that needs judgment.',
47 prompt: `You are a scout: a fast, narrowly scoped reader. ${NO_WRITES}
48Find exactly the facts the task names, in full: do not skip a search, stop early or guess. Where the task names several facts, find each one. ${REPORT_FORMAT} Never suggest fixes.`,
49 tools: READ_TOOLS,
50 omitClaudeMd: true,
51 },
52 {
53 name: 'mapper',
54 model: 'sonnet',
55 effort: 'medium',
56 description:
57 'Basic, well-scoped code mapping and analysis: how a module fits together, what a function does end to end, which files a change touches, a scan that needs some judgment.',
58 prompt: `You are a mapper: you read code and explain how it fits together. ${NO_WRITES}
59Follow the code, not the names: read what a function actually does before describing it. ${REPORT_FORMAT}`,
60 tools: READ_AND_SHELL_TOOLS,
61 },
62 {
63 name: 'planner',
64 model: 'opus',
65 effort: 'medium',
66 description:
67 'Planning, dependency analysis and organization work: an implementation plan for a coding task, the order to do things in, what depends on what, how to structure or split work.',
68 prompt: `You are a planner. ${NO_WRITES}
69Read the code the task touches, check library usage against current documentation (context7) rather than memory, and write a plan a coder can follow without asking questions: the files to change, each change in order, the tests to add or run, and the acceptance criteria. Name the trade-offs you decided and why. Keep the plan as short as it can be while complete.`,
70 tools: READ_AND_SHELL_TOOLS,
71 },
72 {
73 name: 'coder',
74 model: 'sonnet',
75 effort: 'medium',
76 description:
77 'Implements a written plan: the real coding work once a planner has produced the plan. Needs the plan in its prompt.',
78 prompt: `You are a coder: you implement the plan you were given, exactly and completely, following the project's conventions and CLAUDE.md rules.
79Before reporting a change as done, verify it with a check that exercises it: run the tests the plan names, the build, the linter and the formatter, and read the output. A change you did not exercise is not done; say so. Do not commit, push or open pull requests unless the plan says to. Report what you changed, what you verified and how, and anything in the plan you could not do.`,
80 disallowedTools: ['Agent'],
81 },
82 {
83 name: 'analyst',
84 model: 'opus',
85 effort: 'xhigh',
86 description:
87 'Intense code analysis within one sitting: root-cause investigations, tricky bugs, architecture questions, security-oriented review. First stop before a researcher.',
88 prompt: `You are an analyst: you dig into code until you can explain it with evidence. ${NO_WRITES}
89Form hypotheses, test each one against the code and its behaviour (run read-only commands, tests and scripts where they help), and discard the ones that fail. Separate what you verified from what you infer. ${REPORT_FORMAT}`,
90 tools: READ_AND_SHELL_TOOLS,
91 },
92 {
93 name: 'researcher',
94 model: 'fable',
95 effort: 'high',
96 description:
97 'Deep research and long-horizon analysis: multistep web research, whole-repository analysis that needs the full context window, work larger than one sitting, or an analyst that fell short. Slow and costly: use last.',
98 prompt: `You are a researcher: the most capable, most expensive reader on the team, sent in when the question is wide, deep or long. ${NO_WRITES}
99Work the question end to end across every source it needs, keep track of what each source showed, and reconcile disagreements between them. ${REPORT_FORMAT}`,
100 tools: READ_AND_SHELL_TOOLS,
101 },
102]
103
104export const PLUGIN = 'minions'
105
106export const minionType = (minion: Minion): string => `${PLUGIN}:${minion.name}`
107
108/** The routing table the dispatcher reads, one line per minion. */
109export const ROUTING_TABLE = `- Simple fetch, read or scan tasks with checkable answers: minions:scout (Haiku). When the scan needs judgment: minions:mapper (Sonnet).
110- Basic, well-scoped code mapping and analysis: minions:mapper (Sonnet).
111- Planning, dependency analysis and organization work: minions:planner (Opus).
112- Coding work: first minions:planner (Opus) writes the plan, then minions:coder (Sonnet) implements it with the whole plan in its prompt. Never skip the plan, never have the planner code.
113- Intense code analysis (root causes, tricky bugs, architecture, security review): minions:analyst (Opus at xhigh effort).
114- Deep research, whole-repository analysis, work larger than one sitting, or an analyst that fell short: minions:researcher (Fable). Use it last; it is slow and costly.
115- Any other kind of work: ask the user which minion or model to use (AskUserQuestion). Do not guess.`
116hooks/guard.ts 86 lines1// The guards: pure decisions about what the main conversation may do itself.
2// No `$`, no engine: a function of the event's facts, so the tests read them
3// without a session. One reason to change: what counts as doing the work.
4
5import { PLUGIN } from './roster'
6
7/**
8 * The tools the dispatcher keeps: spawning and steering minions, asking the
9 * user, tracking tasks, loading skills and tool schemas, and leaving plan
10 * mode. An allowlist, so a tool this list does not name (every file, shell,
11 * code-execution, search, web and MCP tool, and any tool a later engine adds)
12 * is refused rather than slipping through.
13 */
14const ORCHESTRATION_TOOLS: ReadonlySet<string> = new Set([
15 'Agent',
16 'AskUserQuestion',
17 'SendMessage',
18 'ListAgents',
19 'TaskStop',
20 'FetchInboxMessage',
21 'ToolSearch',
22 'Skill',
23 'TodoWrite',
24 'TaskCreate',
25 'TaskGet',
26 'TaskList',
27 'TaskUpdate',
28 'GetTask',
29 'EnterPlanMode',
30 'ExitPlanMode',
31 'SendFeedback',
32])
33
34/** True when `tool` does work the dispatcher must hand to a minion. */
35export const isWorkTool = (tool: string): boolean => !ORCHESTRATION_TOOLS.has(tool)
36
37/** The facts of a tool call the guard reads. */
38export type ToolCallFacts = {
39 tool: string
40 /** The loop the call runs in; absent on the main conversation. */
41 agentId?: string
42 /** Who raised the call: `engine` for the model's own call, a plugin's name for one its hook made. */
43 origin: { readonly plugin: string }
44}
45
46const ENGINE = 'engine'
47
48const isModelsOwnCall = (call: ToolCallFacts): boolean => call.origin.plugin === ENGINE
49
50/**
51 * Why a tool call in the main conversation is refused, or undefined to let it
52 * run. A minion's call, and a call another plugin makes, always runs.
53 */
54export const toolCallDenial = (call: ToolCallFacts): string | undefined => {
55 if (call.agentId !== undefined || !isModelsOwnCall(call)) return undefined
56 if (!isWorkTool(call.tool)) return undefined
57
58 return `${PLUGIN}: the main conversation never does the work itself. Spawn a minion with the Agent tool: minions:scout for reads and scans, minions:mapper for code mapping, minions:planner for plans, minions:coder for a written plan, minions:analyst for deep code analysis, minions:researcher for deep research. Ask the user when none fits.`
59}
60
61/** The facts of an agent spawn the guard reads. */
62export type SpawnFacts = {
63 subagentType: string
64 model?: string
65 /** The loop the spawn happens in; absent on the main conversation. */
66 parentAgentId?: string
67 fork: boolean
68 isResume: boolean
69 isWorkflow: boolean
70}
71
72const isMinion = (subagentType: string): boolean => subagentType.startsWith(`${PLUGIN}:`)
73
74/**
75 * Why a spawn from the main conversation is refused, or undefined to let it
76 * start: a minion carries its model; any other type must name one, so no
77 * agent runs on a tier nobody chose.
78 */
79export const spawnDenial = (spawn: SpawnFacts): string | undefined => {
80 if (spawn.parentAgentId !== undefined) return undefined
81 if (spawn.fork || spawn.isResume || spawn.isWorkflow) return undefined
82 if (isMinion(spawn.subagentType) || spawn.model !== undefined) return undefined
83
84 return `${PLUGIN}: ${spawn.subagentType} names no model. Use a minions:* type, or pass model (haiku, sonnet, opus or fable) per the routing table; ask the user when no row fits.`
85}
86hooks/models.ts 18 lines1// The model tiers by alias, resolved to the ids a request names. A `turn.step`
2// rewrite takes a full id, not an alias, so the dispatcher option is resolved
3// here; a value that is no alias is taken as an id already.
4
5export const MODEL_IDS = {
6 haiku: 'claude-haiku-5-5',
7 sonnet: 'claude-sonnet-5-5',
8 opus: 'claude-opus-5-5',
9 fable: 'claude-fable-5-1',
10} as const
11
12export type ModelAlias = keyof typeof MODEL_IDS
13
14const isAlias = (value: string): value is ModelAlias => Object.hasOwn(MODEL_IDS, value)
15
16/** The model id for an alias, or `value` itself when it is already an id. */
17export const resolveModelId = (value: string): string => (isAlias(value) ? MODEL_IDS[value] : value)
18