SLOPSHOPPER

tool-compose

Compose Claude Code tool calls in a permission-limited JavaScript script.

newguardtoolprocessnetwork
★ 1v0.1.0no licenseupdated 2026-10-14RanolP/.dotfiles/nix/home/configs/claude/mods/tool-compose
A shopper browsing a rack in a slop shop
README

.dotfiles

Installation

Windows CMD

Requirements:

  • Windows CMD
  • curl executable
  • Winget executable (>= v1.6.2631, follow instruction from winget repository if you don't have or have a lower version)
curl -L dotfiles.ranolp.dev/setup | cmd /Q

Windows PowerShell (TODO)

Requirements:

  • Windows PowerShell
  • Winget executable (>= v1.6.2631, follow instruction from winget repository if you don't have or have a lower version)
curl dotfiles.ranolp.dev/setup | iex

Windows ArchWSL (WIP)

Requirements:

  • ArchWSL Bash
  • curl executable
curl -L dotfiles.ranolp.dev/setup | sh

macOS (TODO)

Requirements:

  • macOS zsh
  • curl executable
curl -L dotfiles.ranolp.dev/setup | sh
Source 2 files
hooks/register.ts 360 lines
1import type { EngineInterface, Register, ToolInfo, ToolCallResult } from 'claude-code'
2import { READY_LINE } from '../daemon/protocol.mjs'
3
4export const EXEC_TOOL = 'mcp__tool-compose__exec'
5export const SCRIPT_TIMEOUT_MS = 30_000
6export const MAX_RESULT_CHARS = 50_000
7const BUDGET_RESERVE_MS = 150
8
9export const TOOL_DESCRIPTION = `Run JavaScript that composes Claude Code tools in one model request. Use exec for 3+ dependent or fan-out tool calls, or when you can filter large tool results before they reach context. Call any tool by its name with tools.<name>(input); use await tools.list() to inspect available tools. Tool calls still pass through Claude Code permissions and hooks.
10
11Example:
12const read = await tools.Read({file_path:"README.md"});
13const matches = await tools.Grep({pattern:"TODO", path:"src"});
14return {firstLine:read.content.split("\\n")[0], matches};`
15
16type HttpJson = { status: number; ok: boolean; text: string }
17export type ExecEngine = {
18  http: Pick<EngineInterface['http'], 'fetch'>
19  tool: Pick<EngineInterface['tool'], 'list' | 'call'>
20  fs: { read(path: string): Promise<string> }
21  env: Pick<EngineInterface['env'], 'get'>
22  session: Pick<EngineInterface['session'], 'id'>
23  process: Pick<EngineInterface['process'], 'spawn'>
24  ui: Pick<EngineInterface['ui'], 'log'>
25}
26type ExecNext = { signal: AbortSignal; budget: { remainingMs: number } }
27
28declare global {
29  interface PromiseConstructor {
30    withResolvers<T>(): { promise: Promise<T>; resolve(value: T | PromiseLike<T>): void; reject(reason?: unknown): void }
31  }
32}
33
34function logFailure($: { ui: Pick<EngineInterface['ui'], 'log'> }, operation: string, status: string | number, body: string): void {
35  try { $.ui.log(`[tool-compose] ${operation} failed; status=${status}; body=${body.slice(0, 4000)}`, { to: 'debug' }) } catch {}
36}
37
38function isRecord(value: unknown): value is Record<string, unknown> {
39  return value !== null && typeof value === 'object' && !Array.isArray(value)
40}
41
42export function encodeFinal(value: unknown, maxChars = MAX_RESULT_CHARS): string {
43  let encoded: string
44  try {
45    const result = JSON.stringify(value)
46    encoded = result === undefined ? 'null' : result
47  } catch (err) {
48    encoded = JSON.stringify({ error: 'result is not JSON serializable', detail: errorText(err) })
49  }
50  if (encoded.length <= maxChars) return encoded
51  return `${encoded.slice(0, maxChars)}\n[tool-compose: output truncated at ${maxChars} characters]`
52}
53
54function errorText(err: unknown): string {
55  return err instanceof Error ? `${err.name}: ${err.message}${err.stack ? `\n${err.stack}` : ''}` : String(err)
56}
57
58function daemonSource(root: string): string {
59  return `${root}/daemon/tool-compose.mjs`
60}
61
62function protocolSource(root: string): string {
63  return `${root}/daemon/protocol.mjs`
64}
65
66function hash(text: string): string {
67  let h = 0x811c9dc5
68  for (let i = 0; i < text.length; i++) h = Math.imul(h ^ text.charCodeAt(i), 0x01000193) >>> 0
69  return h.toString(16).padStart(8, '0')
70}
71
72async function fingerprint($: Pick<ExecEngine, 'fs' | 'ui'>, root: string): Promise<string> {
73  try { return hash(`${await $.fs.read(daemonSource(root))}\n${await $.fs.read(protocolSource(root))}`) }
74  catch (err) {
75    logFailure($, 'fingerprint daemon files', 'n/a', `${errorText(err)}; daemon=${daemonSource(root)}; protocol=${protocolSource(root)}`)
76    return 'unread'
77  }
78}
79
80async function socketPath($: Pick<ExecEngine, 'env' | 'session'>): Promise<string> {
81  const home = (await $.env.get('HOME')) ?? '/tmp'
82  const sessionId = await $.session.id()
83  return `${home}/.claude-work/run/tc-${hash(sessionId)}/s.sock`
84}
85
86let daemonReady: Promise<void> | undefined
87let daemonStarting: Promise<void> | undefined
88let activeSocket = ''
89
90function startDaemon($: Pick<ExecEngine, 'process' | 'ui'>, sock: string, source: string, sourceHash: string): Promise<void> {
91  let settled = false
92  const { promise: ready, resolve: readyResolve, reject: readyReject } = Promise.withResolvers<void>()
93  const fail = (err: Error) => {
94    if (!settled) { settled = true; readyReject(err) }
95    if (daemonReady === ready) daemonReady = undefined
96    logFailure($, 'daemon process', 'n/a', err.message)
97  }
98
99  void (async () => {
100    let status = 'no exit status'
101    let stderr = ''
102    try {
103      const stream = $.process.spawn({ argv: ['node', source, sock, sourceHash] })
104      for await (const chunk of stream) {
105        if (chunk.stream === 'stderr') stderr = (stderr + chunk.text).slice(-3000)
106        for (const line of chunk.text.trimEnd().split('\n')) {
107          if (line.includes(READY_LINE) && !settled) { settled = true; readyResolve() }
108          if (line) $.ui.log(`[tool-compose daemon ${chunk.stream}] ${line.slice(0, 3900)}`, { to: 'debug' })
109        }
110      }
111      const result = await stream.result
112      status = `code=${result.code ?? 'unknown'} signal=${result.signal ?? 'none'}`
113      fail(new Error(`daemon exited (${status}); stderr=${stderr || '(empty)'}`))
114    } catch (err) {
115      fail(new Error(`daemon startup/stream failed; status=${status}; stderr=${stderr || '(empty)'}; ${errorText(err)}`))
116    }
117  })()
118  return ready
119}
120
121async function ensureDaemon($: ExecEngine, root: string): Promise<string> {
122  const sock = activeSocket || (activeSocket = await socketPath($))
123  const socketBytes = new TextEncoder().encode(sock).length
124  if (socketBytes > 103) throw new Error(`Unix socket path is ${socketBytes} bytes; the Darwin sun_path limit is 103 bytes: ${sock}`)
125  if (daemonReady) { await daemonReady; return sock }
126  if (daemonStarting) { await daemonStarting; return sock }
127  const starting = (async () => {
128    const source = daemonSource(root)
129    const sourceHash = await fingerprint($, root)
130    let probeStatus: string | number = 'n/a'
131    let probeBody = ''
132    try {
133      const health = await $.http.fetch('http://tool-compose/health', { method: 'GET', socketPath: sock })
134      probeStatus = health.status
135      probeBody = health.text
136      if (health.ok) {
137        const body: unknown = JSON.parse(health.text)
138        if (isRecord(body) && body.fingerprint === sourceHash) { daemonReady = Promise.resolve(); return }
139        $.ui.log(`[tool-compose] daemon fingerprint changed; status=${health.status}; body=${health.text.slice(0, 2000)}`, { to: 'debug' })
140      } else logFailure($, 'daemon health probe', health.status, health.text)
141    } catch (err) {
142      $.ui.log(`[tool-compose] daemon health probe missed; status=${probeStatus}; body=${probeBody || errorText(err)}`, { to: 'debug' })
143    }
144    const ready = startDaemon($, sock, source, sourceHash)
145    daemonReady = ready
146    try { await ready }
147    catch (err) {
148      if (daemonReady === ready) daemonReady = undefined
149      throw err
150    }
151  })()
152  daemonStarting = starting
153  try { await starting }
154  finally { if (daemonStarting === starting) daemonStarting = undefined }
155  return sock
156}
157
158async function fetchJson($: Pick<ExecEngine, 'http' | 'ui'>, sock: string, route: string, token: string, body?: unknown): Promise<Record<string, unknown>> {
159  let text = ''
160  let status: string | number = 'n/a'
161  let logged = false
162  try {
163    const headers = { 'x-tool-compose-token': token, ...(body === undefined ? {} : { 'content-type': 'application/json' }) }
164    const init = body === undefined
165      ? { method: 'GET', socketPath: sock, headers }
166      : { method: 'POST', socketPath: sock, headers, body: JSON.stringify(body) }
167    const response: HttpJson = await $.http.fetch(`http://tool-compose${route}`, init)
168    text = response.text
169    status = response.status
170    if (!response.ok) {
171      logFailure($, route, response.status, response.text)
172      logged = true
173      throw new Error(`HTTP ${response.status}: ${response.text}`)
174    }
175    const parsed: unknown = JSON.parse(response.text)
176    if (!isRecord(parsed)) throw new Error(`expected JSON object; status=${status}; body=${text}`)
177    return parsed
178  } catch (err) {
179    if (!logged) logFailure($, route, status, text || errorText(err))
180    throw err
181  }
182}
183
184function runId(): string {
185  return `${crypto.randomUUID()}${crypto.randomUUID()}`
186}
187
188function controlToken(): string {
189  return `${crypto.randomUUID()}${crypto.randomUUID()}`
190}
191
192function awaitAbort<T>(signal: AbortSignal, work: Promise<T>): Promise<T> {
193  if (signal.aborted) return Promise.reject(new Error('exec interrupted'))
194  let abortListener = () => {}
195  const aborted = new Promise<never>((_, reject) => {
196    abortListener = () => reject(new Error('exec interrupted'))
197    signal.addEventListener('abort', abortListener, { once: true })
198  })
199  return Promise.race([work, aborted]).finally(() => signal.removeEventListener('abort', abortListener))
200}
201
202function toolInput(name: string, input: unknown): Record<string, unknown> {
203  if (!isRecord(input)) throw new TypeError(`tools.${name} expects one object argument`)
204  return { ...input, tool: name }
205}
206
207function isToolCallInput(v: Record<string, unknown>, names: Set<string>): v is Parameters<EngineInterface['tool']['call']>[0] {
208  return typeof v.tool === 'string' && names.has(v.tool)
209}
210
211async function relay(
212  $: ExecEngine,
213  message: Record<string, unknown>,
214  names: Set<string>,
215  signal: AbortSignal,
216): Promise<{ callId: string; reply: Record<string, unknown> }> {
217  const callId = typeof message.callId === 'string' ? message.callId : ''
218  const name = typeof message.tool === 'string' ? message.tool : ''
219  if (!callId || !name || !isRecord(message.input)) throw new Error(`daemon returned a malformed tool call: ${encodeFinal(message, 2000)}`)
220
221  if (name === EXEC_TOOL || name === 'exec') {
222    const reply = { ok: false, error: 'recursive calls to tool-compose exec are refused' }
223    logFailure($, 'recursive exec call', 403, reply.error)
224    return { callId, reply }
225  }
226  if (!names.has(name)) {
227    const reply = { ok: false, error: `unknown Claude Code tool: ${name}` }
228    logFailure($, `unknown relayed tool ${name}`, 400, reply.error)
229    return { callId, reply }
230  }
231  const call = toolInput(name, message.input)
232  if (!isToolCallInput(call, names)) {
233    const reply = { ok: false, error: `unknown Claude Code tool: ${name}` }
234    logFailure($, `invalid relayed tool ${name}`, 400, reply.error)
235    return { callId, reply }
236  }
237  try {
238    const result = await awaitAbort(signal, $.tool.call(call))
239    if ('deny' in result && result.deny !== undefined) {
240      const reply = { ok: false, error: result.deny }
241      logFailure($, `relayed tool ${name}`, 403, result.deny)
242      return { callId, reply }
243    }
244    return { callId, reply: { ok: true, value: result.result } }
245  } catch (err) {
246    if (signal.aborted) throw err
247    const body = errorText(err)
248    logFailure($, `relayed tool ${name}`, 'n/a', body)
249    return { callId, reply: { ok: false, error: body } }
250  }
251}
252
253export async function handleExec($: ExecEngine, root: string, script: unknown, next: ExecNext): Promise<ToolCallResult> {
254  if (typeof script !== 'string') {
255    logFailure($, 'invalid exec input', 400, 'script must be a string')
256    return { result: encodeFinal({ error: 'script must be a string' }), isError: true }
257  }
258  if (next.signal.aborted) {
259    logFailure($, 'exec interrupted', 'n/a', 'dispatch signal was already aborted')
260    return { result: encodeFinal({ error: 'exec interrupted' }), isError: true }
261  }
262  const remaining = next.budget.remainingMs
263  const budgetLimit = Number.isFinite(remaining) ? Math.floor(remaining - BUDGET_RESERVE_MS) : SCRIPT_TIMEOUT_MS
264  const timeoutMs = Math.min(SCRIPT_TIMEOUT_MS, budgetLimit)
265  if (timeoutMs < 1) {
266    const body = `insufficient hook budget (${Math.max(0, remaining)} ms remaining)`
267    logFailure($, 'exec budget', 'n/a', body)
268    return { result: encodeFinal({ error: body }), isError: true }
269  }
270
271  let sock = ''
272  const id = runId()
273  const token = controlToken()
274  let runSubmitted = false
275  let cancelled = false
276  let cancelFlight: Promise<void> | undefined
277  const cancelRun = () => {
278    cancelled = true
279    if (!sock || !runSubmitted) return Promise.resolve()
280    if (!cancelFlight) {
281      cancelFlight = fetchJson($, sock, '/cancel', token, { runId: id, controlToken: token }).then(() => {}).catch(err => {
282        logFailure($, `cancel run ${id}`, 'n/a', errorText(err))
283      })
284    }
285    return cancelFlight
286  }
287  const onAbort = () => { void cancelRun() }
288  next.signal.addEventListener('abort', onAbort, { once: true })
289  try {
290    sock = await ensureDaemon($, root)
291    if (next.signal.aborted) throw new Error('exec interrupted')
292    const tools: ToolInfo[] = await $.tool.list()
293    const names = new Set(tools.map(tool => tool.name))
294    runSubmitted = true
295    const started = await fetchJson($, sock, '/run', token, { runId: id, controlToken: token, script, tools, timeoutMs })
296    if (started.runId !== id) throw new Error(`daemon started unexpected run ${String(started.runId)}`)
297    if (cancelled || next.signal.aborted) throw new Error('exec interrupted')
298
299    for (;;) {
300      const message = await fetchJson($, sock, `/next?runId=${encodeURIComponent(id)}`, token)
301      if (message.kind === 'call') {
302        const { callId, reply } = await relay($, message, names, next.signal)
303        if (next.signal.aborted) throw new Error('exec interrupted')
304        const delivered = await fetchJson($, sock, '/result', token, { runId: id, callId, reply })
305        if (delivered.delivered !== true) logFailure($, `return result for ${String(message.tool)}`, 409, encodeFinal(delivered, 2000))
306        continue
307      }
308      if (message.kind === 'done') {
309        const closed = await fetchJson($, sock, '/close', token, { runId: id })
310        if (closed.closed !== true) logFailure($, `close run ${id}`, 409, encodeFinal(closed, 2000))
311        if (message.ok === true) return { result: encodeFinal(message.value) }
312        return { result: encodeFinal({ error: message.error ?? 'script failed', stack: message.stack ?? '' }), isError: true }
313      }
314      if (message.kind === 'pending') continue
315      throw new Error(`unexpected daemon response: ${encodeFinal(message, 2000)}`)
316    }
317  } catch (err) {
318    logFailure($, `exec ${id}`, 'n/a', errorText(err))
319    daemonReady = undefined
320    if (runSubmitted || sock) await cancelRun()
321    return { result: encodeFinal({ error: errorText(err) }), isError: true }
322  } finally {
323    next.signal.removeEventListener('abort', onAbort)
324    if (cancelled) await cancelFlight
325  }
326}
327
328export const register: Register = on => {
329  on('session.start', async ($, e, next) => {
330    const started = await next(e)
331    try { await ensureDaemon($, $.plugin.root) }
332    catch (err) { logFailure($, 'session startup', 'n/a', errorText(err)) }
333    try {
334      await $.tool.register({
335        name: 'exec',
336        description: TOOL_DESCRIPTION,
337        inputSchema: {
338          type: 'object',
339          properties: { script: { type: 'string', description: 'JavaScript source. Use await tools.ToolName({...}) and return the value to send back.' } },
340          required: ['script'],
341          additionalProperties: false,
342        },
343      })
344    } catch (err) {
345      logFailure($, 'register exec tool', 'n/a', errorText(err))
346      throw err
347    }
348    return started
349  })
350
351  on('tool.call', { tool: EXEC_TOOL }, async ($, e, next) => {
352    const { script } = e
353    if (typeof script !== 'string') {
354      logFailure($, 'invalid exec input', 400, 'script must be a string')
355      return { result: encodeFinal({ error: 'script must be a string' }), isError: true }
356    }
357    return handleExec($, $.plugin.root, script, next)
358  })
359}
360
daemon/protocol.mjs 3 lines
1export const READY_LINE = 'TOOL_COMPOSE_READY'
2export const SANDBOX_PROFILE = '(version 1) (allow default) (deny network*) (deny signal) (allow signal (target self))'
3