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.

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.
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.<session cwd>/markers.jsonl via $.fs.| Question | Result | Evidence |
|---|---|---|
Does a tool.call rewrite of name reach the spawn? | Yes: agent.spawn sees name: "Hopper", reached: true | evidence/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 agent | same |
| Does the task tree show the name? | Yes: Hopper Hopper · Sleep 15 seconds and reply; finish notice quotes the label | evidence/tree-f10.txt |
Is CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 still needed? | No: the run had no flag set and user settings skipped | same 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 overhead | tool.call 30 ms and agent.spawn 28 ms, including the probe's own file writes | debug excerpt |
directory source, which loads from the source path, not the cache).name (the model never passed one in these runs).--setting-sources project,local).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-toolcallhooks/probe.ts 76 lines1// 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