SLOPSHOPPER

minions

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…

newguardcommandstatuspromptagents
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · minions
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Read(/work/app/src/auth.ts) ⎿ Denied by minions: minions: the main conversation never does the work itself. Spawn a minion with the Agen ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /minions ⎿ minions: minions: dispatcher on haiku; guard on (/config › minions). ⎿ minions: ⎿ minions: Routing table: ⎿ minions: - Simple fetch, read or scan tasks with checkable answers: minions:scout (Haiku). When the scan needs judgment ⎿ minions: - Basic, well-scoped code mapping and analysis: minions:mapper (Sonnet). ⎿ minions: - Planning, dependency analysis and organization work: minions:planner (Opus). ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ minions: minions: haiku
README

minions

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.

Install

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

What it does

  • The main conversation runs on Haiku. Every request the main loop makes is rewritten to the dispatcher model (turn.step), whatever /model says. Minions keep their own models. The status line shows minions: haiku.
  • The dispatcher never does the work. Its system prompt carries the routing table and the rule, and with the guard on (the default) every tool but the orchestration tools is refused to it with a message naming the minion to spawn instead. The guard is an allowlist (Agent, AskUserQuestion, SendMessage, ListAgents, TaskStop, ToolSearch, Skill, the task-list tools, plan mode), so every file, shell, code-execution, search, web and MCP tool is refused, and so is any tool a later engine release adds.
  • Six minion types are registered, each pinned to a model and an effort:
MinionModelEffortFor
minions:scoutHaikumediumSimple fetch, read and scan tasks with checkable answers
minions:mapperSonnetmediumBasic, well-scoped code mapping and analysis; scans that need judgment
minions:plannerOpusmediumPlanning, dependency analysis, organization work; writes the plan a coder follows
minions:coderSonnetmediumImplements a written plan, then verifies it with a check that exercises it
minions:analystOpusxhighIntense code analysis within one sitting: root causes, tricky bugs, architecture, security review
minions:researcherFablehighDeep 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.

  • Coding work is two minions: a planner writes the plan, then a coder implements it with the whole plan in its prompt.
  • Anything the table does not cover is the user's call: the dispatcher asks with AskUserQuestion rather than guessing.
  • A spawn from the main conversation that is not a minion must name its 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.

Why these tiers

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:

  • Haiku 5.5 is Anthropic's pick for narrowly scoped subagent work with checkable outputs; scans that need judgment go to Sonnet 5.5, so the mapper exists.
  • Opus 5.5 at medium effort is the recommended starting point for most agent workloads and for planning; Sonnet 5.5 is strongest at well-scoped, everyday tasks, which is what a written plan gives it. The Opus-plan, Sonnet-implement split is the pattern Claude Code ships as the opusplan alias.
  • Opus 5.5 at higher effort matches Fable 5.1 on most analysis at a fraction of the cost, so the analyst is the first stop and the Fable researcher is the escalation: multistep research, the full context window, work larger than one sitting.

Commands

CommandDescription
/minionsThe routing table, the dispatcher model and the guard state

Options

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.

Notes

  • The dispatcher is cheap but small: give it one task at a time, and expect it to ask when a task fits no row.
  • Built-in agent types (Explore, Plan, general-purpose) still exist; the dispatcher must pass model to use one.
  • Development: claude --plugin-dir plugins/minions, claude plugin validate plugins/minions, claude plugin test plugins/minions.
Source 4 files
hooks/register.ts 105 lines
1// 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}
105
hooks/roster.ts 116 lines
1// 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.`
116
hooks/guard.ts 86 lines
1// 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}
86
hooks/models.ts 18 lines
1// 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