Probe-only: records each spawned agent's name from $.agent.list() as JSONL markers, so a headless run of the names mod can be judged from files.

Retired (2026-10-06). Replaced by named-subagents-mod, a Claude Code mod that needs Claude Code 2.1.287 or later and no Python. 0.7.2 is the last Python release; this repository is archived and gets no further fixes.
Distinct, themed, non-repeating names for parallel Claude Code subagents — a userspace port of Codex's per-instance nickname_candidates.
<img src="https://raw.githubusercontent.com/Bobby-cell-commits/named-subagents/master/assets/demo.gif" alt="A real Claude Code 2.1.283 session fanning out three subagents; the task tree shows them as Backus, Chekhov and Bosch" width="760">
Fan out several subagents in Claude Code and the task tree shows each one by its type. This plugin gives every instance its own name, themed to the kind of task and never repeated across runs. (Recorded from a real session; the banner and status line are cropped out.)
claude plugin marketplace add Bobby-cell-commits/named-subagents
claude plugin install named-subagents@named-subagents
Needs python3 (3.8+) on PATH. New sessions name every subagent, for any agent type.
The plugin runs four hooks:
PreToolUse sets the Agent tool's name field (shown in the tree) and the <emoji> Name · task label (quoted in finish notices). Two live agents never share a name, because name is also the address SendMessage routes by; other local sessions' titles are skipped too. A name the model chose is left alone.SubagentStart tells the agent its name and asks it to open its report with [Name].SubagentStop frees the name by agent ID, so a resumed agent keeps its name.Stop fails loud: if Claude Code did not record the name the hook set (for example, a future version stops honoring name), you see a named-subagents: … notice at the end of the turn.It adds nothing to the prompt: no agent files, no extra agent-list entries. NAMED_SUBAGENTS_HOOK_DISABLE=1 turns it off.
If you registered hooks earlier with named-subagents hook install, run named-subagents hook uninstall; while that install is present the plugin's hooks stand down so the two never name one dispatch.
Known limits. name is an Agent-tool input that is not in the published schema, so an update could drop it (the Stop check exists for that). If you refuse an Agent permission prompt and a same-type dispatch starts within 30 seconds, that agent may be told the refused name; SubagentStop repairs the record and shows a mix-up notice. Details and live verification: CHANGELOG.
pip install named-subagents
named-subagents hook install # name mode: the plugin's four hooks, in ~/.claude/settings.json
named-subagents hook status
hook install --context-only registers the older namer instead (deprecated, removal planned for 0.8): the agent learns its name, but the tree does not show it. With the plugin enabled, a context-only install turns the tree names off, because the plugin steps aside for every event these hooks cover. hook install --project . scopes either to one project; hook uninstall removes them. install/uninstall back up settings.json, refuse to touch malformed JSON, and only add or remove their own entries. Hooks load at session start, so open a new session afterwards. Install one runtime's hooks per machine (the ports lock state differently).
| Env var | Effect |
|---|---|
NAMED_SUBAGENTS_HOOK_DISABLE=1 | pause the hooks without uninstalling |
NAMED_SUBAGENTS_LEDGER | ledger path (default ~/.local/state/named-subagents/hook-ledger.json) |
NAMED_SUBAGENTS_QUEUE_DIR | dispatch-queue dir (default ~/.local/state/named-subagents/queue/) |
NAMED_SUBAGENTS_HOOK_BIO=1 | add the figure's one-line bio to the identity block |
NAMED_SUBAGENTS_PLUGIN_FORCE=1 | run the plugin's hooks even next to a settings.json install |
395 names in 14 categories, each globally unique, each with a one-line bio:
| Category | Task shape | Theme | e.g. |
|---|---|---|---|
explore | map / search a codebase | Explorers & navigators | Magellan, Shackleton |
code | implement features | Programmers & computing pioneers | Turing, Hopper |
research | external info gathering | Scientists | Curie, Feynman |
reflect | design rationale | Philosophers | Socrates, Kant |
debug | root-cause hunting | Detectives | Holmes, Poirot |
test | edge cases, adversarial | Tricksters | Loki, Anansi |
review | critique, verdict | Judges & jurists | Solomon, Ginsburg |
security | audit, threat model | Guardians & sentinels | Argus, Heimdall |
design | UI / UX / visual | Artists & designers | DaVinci, Rams |
data | analysis, stats, ML | Mathematicians | Gauss, Noether |
orchestrate | plan, coordinate | Strategists | SunTzu, Napoleon |
docs | technical writing | Writers | Orwell, Borges |
build | infra / refactor / perf | Engineers & inventors | Tesla, Brunel |
default | catch-all | Stars | Orion, Vega |
A task maps to a category by explicit category > subagent_type > task keywords > default. The keyword layer is a heuristic; pass category= or role= when the theme must be exact.
Non-repeat. allocate() draws in a deterministic md5-seeded order, records used names in a ledger, and skips them next time. When a pool runs out it starts a new generation (Magellan·2, …). A name is never reused unless you release it. Same (category, ledger state) gives the same result, so re-runs are safe.
<img src="https://raw.githubusercontent.com/Bobby-cell-commits/named-subagents/master/assets/library-demo.gif" alt="examples/demo.py: four fan-out rounds with themed, non-repeating names and the ledger summary" width="640">
pip install named-subagents # Python 3.8+, zero dependencies
from named_subagents import Registry, Ledger, plan_fanout
reg = Registry.load()
ledger = Ledger(".named-subagents-ledger.json")
plan = plan_fanout(["map the auth module", "map the billing module"],
reg, ledger=ledger, role="Explore")
for a in plan:
print(a.emoji, a.nickname, "—", a.bio) # a.agent_kwargs() -> Agent-tool payload
The generated prompt asks the agent to open with [Name]; attribute(nickname, report) repairs a missing or wrong prefix when all you have is the report text.
named-subagents resolve --task "audit auth" --explain # which theme, and why
named-subagents allocate --category reflect --count 3
named-subagents assign --role Explore --task "map the router" --count 4 \
--ledger .ledger.json [--format agent|labels|workflow|swarm|table]
named-subagents assign --task "audit the release" --pin security=Argus # stable identity
named-subagents release --category explore --name Hudson --ledger .ledger.json # recycle
named-subagents retire --category explore --name Columbus --ledger .ledger.json # never again
named-subagents bio Heimdall
named-subagents stats --ledger .ledger.json
named-subagents doctor # self-checks; --json for machines
named-subagents init # scaffold a config
--avoid-installed keeps names disjoint from your .claude/agents names.--format emits snippets for Workflow scripts, swarm YAML, or plain labels.doctor checks registry integrity, ledger health, pins and version strings, and self-tests the hooks./named-fanout skill: cp -r skill/named-fanout ~/.claude/skills/.Config comes from --config PATH, $NAMED_SUBAGENTS_CONFIG, or ~/.config/named-subagents/config.json:
{ "pins": { "security": "Argus" },
"categories": { "starships": { "theme": "Star systems", "emoji": "🚀",
"keywords": ["fleet"], "names": ["Enterprise", "Rocinante"] } },
"extend": { "explore": { "names": ["Kupe"] } } }
New keys add categories, existing keys replace them, extend appends. Custom names are validated on load because they end up inside agent prompts. A project-local ./.named-subagents.json is not loaded unless you pass --cwd-config (or set NAMED_SUBAGENTS_CWD_CONFIG=1), since a cloned repo controls it. See SECURITY.md.
python3 tests/test_named_subagents.py && python3 tests/test_hook.py
python3 tests/test_name_mode.py && python3 tests/test_roster.py
CI runs the suites on Python 3.8/3.12/3.13, plus ruff and coverage. See CONTRIBUTING.md for the ground rules and docs/RELEASING.md for releases. The npm package and its JavaScript port were retired in 0.7.1. Design notes live in docs/research/; docs/COMMUNITY.md surveys the rest of the ecosystem, which names agents by role, not instance.
hooks/witness.ts 40 lines1// Witness for mod/ (named-subagents-mod). Appends JSONL to <session cwd>/markers.jsonl:
2// tool.call.Agent the name the call carries when it reaches this hook (order-dependent)
3// agent.spawn e.name at spawn, then the list row's name for the new id
4// turn.complete each subagent's listed name when its loop ends ("did it stick")
5import type { Register, EngineInterface } from 'claude-code';
6
7let cwd: string | undefined;
8let chain: Promise<void> = Promise.resolve();
9function mark($: EngineInterface, hook: string, data: Record<string, unknown>): Promise<void> {
10 const path = `${cwd ?? $.plugin.root}/markers.jsonl`;
11 const line = JSON.stringify({ ts: new Date().toISOString(), hook, ...data }) + '\n';
12 chain = chain.then(async () => {
13 const prev = (await $.fs.exists(path)) ? await $.fs.read(path) : '';
14 await $.fs.write(path, prev + line);
15 }).catch(() => undefined); // audit-allow: fail-loud — probe telemetry; a lost line shows as a gap
16 return chain;
17}
18
19export const register: Register = (on) => {
20 on('session.start', async ($, e, next) => {
21 cwd = e.cwd;
22 const r = await next(e);
23 await mark($, 'session.start', { version: await $.session.version() });
24 return r;
25 });
26 on('agent.spawn', async ($, e, next) => {
27 const r = await next(e);
28 const row = (await $.agent.list()).find(a => a.id === r.agentId);
29 await mark($, 'agent.spawn', { tool_use_id: e.tool_use_id, spawnName: e.name, agentId: r.agentId, listName: row?.name, type: e.subagentType, description: e.description });
30 return r;
31 });
32 on('turn.complete', async ($, e, next) => {
33 if (e.agentId !== undefined) {
34 const row = (await $.agent.list()).find(a => a.id === e.agentId);
35 await mark($, 'turn.complete', { agentId: e.agentId, listName: row?.name, status: row?.status });
36 }
37 return next(e);
38 });
39};
40