SLOPSHOPPER

mods-name-toolcall

Throwaway probe: does a tool.call rewrite of the Agent tool's `name` reach the spawn, the agent list and the task tree? Writes JSONL markers.

newguardagents
A shopper browsing a rack in a slop shop
README

mods-name-toolcall (throwaway)

Claude Mods (function hooks) probe for named-subagents: can a mod set the Agent tool's name from a tool.call hook, and bind it to the agent exactly? Tested on Claude Code 2.1.287, 2026-10-02. Do not ship.

What the mod does (hooks/probe.ts)

  • tool.call{Agent} → if the model passed no name, calls next({ ...e, name, description: "<name> · <desc>" }) with a name from a 6-name pool. A model-supplied name is left alone.
  • agent.spawn → logs e.name, awaits next(e) for the agentId, then reads $.agent.list() and records whether that id carries the hook-set name.
  • turn.complete → logs the subagent's agentId, name and status.
  • Every fire appends one JSONL line to <session cwd>/markers.jsonl via $.fs.

Results (all passed)

QuestionResultEvidence
Does a tool.call rewrite of name reach the spawn?Yes: agent.spawn sees name: "Hopper", reached: trueevidence/headless-noflag.markers.jsonl
Does next(e) in agent.spawn give the id, and does the agent list show the name on it?Yes: nameStuck: true about 10 ms after the spawn, for every agentsame
Does the task tree show the name?Yes: Hopper Hopper · Sleep 15 seconds and reply; finish notice quotes the labelevidence/tree-f10.txt
Is CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 still needed?No: the run had no flag set and user settings skippedsame headless run
Does a marketplace-installed plugin load modules?Yes: installed from a local directory marketplace at --scope local, no --plugin-dir; debug log says hooks module mods-name-toolcall@modsprobe loaded (worker, environment 1, tier user)evidence/marketplace-install.*
Hook overheadtool.call 30 ms and agent.spawn 28 ms, including the probe's own file writesdebug excerpt

Not tested

  • A GitHub-sourced marketplace (the test used a directory source, which loads from the source path, not the cache).
  • A model-supplied name (the model never passed one in these runs).
  • Foreground and nested dispatches, SendMessage resume, two sessions allocating at once.
  • Coexistence with the shipped Python hooks (all runs used --setting-sources project,local).

Re-run

bash probes/mods-name-toolcall/run.sh <label> <runs_root>
SECS=45 bash probes/naming-alternatives/tui_capture.sh modsname <workdir> <frames_dir> "<prompt>" \
  --plugin-dir probes/mods-name-toolcall
Source 1 files
hooks/probe.ts 76 lines
1// mods-name-toolcall — throwaway. Answers, for Claude Code 2.1.287 function hooks:
2//   Q1  does a `tool.call` rewrite of the Agent tool's `name` reach `agent.spawn` (e.name)?
3//   Q2  does `next(e)` in agent.spawn give the agentId, and does `$.agent.list()` then
4//       report that id with the hook-set name (exact name→agent binding, fail-loud check)?
5//   Q3  is a model-supplied `name` visible to the hook (so it can be left alone)?
6//
7// Witnessing rule: every hook fire appends a JSONL line to <session cwd>/markers.jsonl
8// via $.fs (hooks run with no Node). Never judge from $.ui.log.
9
10import type { Register, EngineInterface } from 'claude-code';
11
12type Json = Record<string, unknown>;
13
14const POOL = ['Hopper', 'Lovelace', 'Turing', 'Backus', 'Knuth', 'Liskov'];
15let picked = 0;
16let cwd: string | undefined;
17const sentName: Record<string, string> = {}; // tool_use_id -> name the hook set
18let writeChain: Promise<void> = Promise.resolve();
19
20// Serialized read-append-write: $.fs.write replaces the file, so append = read + write.
21function mark($: EngineInterface, hook: string, data: Json): Promise<void> {
22  const path = `${cwd ?? $.plugin.root}/markers.jsonl`;
23  const line = JSON.stringify({ ts: new Date().toISOString(), hook, ...data }) + '\n';
24  writeChain = writeChain.then(async () => {
25    const prev = (await $.fs.exists(path)) ? await $.fs.read(path) : '';
26    await $.fs.write(path, prev + line);
27  }).catch(() => undefined); // audit-allow: fail-loud — probe telemetry; a lost line shows as a gap
28  return writeChain;
29}
30
31export const register: Register = (on) => {
32  on('session.start', async ($, e, next) => {
33    cwd = e.cwd;
34    const r = await next(e);
35    await mark($, 'session.start', { cwd: e.cwd, root: $.plugin.root, version: await $.session.version() });
36    return r;
37  });
38
39  on('tool.call', { tool: 'Agent' }, async ($, e, next) => {
40    const modelName = e.name;
41    const name = modelName ?? POOL[picked++ % POOL.length];
42    if (modelName === undefined && e.tool_use_id !== undefined) sentName[e.tool_use_id] = name;
43    await mark($, 'tool.call.Agent', {
44      tool_use_id: e.tool_use_id, loop: e.agentId, subagent_type: e.subagent_type,
45      description: e.description, modelName, setName: modelName === undefined ? name : null,
46    });
47    const r = await next(modelName === undefined ? { ...e, name, description: `${name} · ${e.description}` } : e);
48    await mark($, 'tool.call.Agent.result', { tool_use_id: e.tool_use_id, deny: r.deny, isError: r.isError });
49    return r;
50  });
51
52  on('agent.spawn', async ($, e, next) => {
53    const expected = sentName[e.tool_use_id];
54    await mark($, 'agent.spawn', {
55      tool_use_id: e.tool_use_id, name: e.name, expected, reached: e.name === expected,
56      description: e.description, subagentType: e.subagentType, background: e.background,
57    });
58    const r = await next(e);
59    const list = await $.agent.list();
60    const row = list.find(a => a.id === r.agentId);
61    await mark($, 'agent.spawn.bound', {
62      agentId: r.agentId, deny: r.deny, expected, listed: row ?? null,
63      nameStuck: row !== undefined && row.name === expected,
64    });
65    return r;
66  });
67
68  on('turn.complete', async ($, e, next) => {
69    if (e.agentId !== undefined) {
70      const row = (await $.agent.list()).find(a => a.id === e.agentId);
71      await mark($, 'turn.complete', { agentId: e.agentId, name: row?.name, status: row?.status });
72    }
73    return next(e);
74  });
75};
76