SLOPSHOPPER

code-mode

Compose existing Claude Code tools in isolated JavaScript, preserving tool permissions and returning compact output.

guardtoolprocess
v0.2.2MITupdated 2026-10-20jpenley/AITuning/mods/code-mode
A shopper browsing a rack in a slop shop
README

Code mode for Claude Code

Compose existing session tools in isolated JavaScript and return a compact answer. The mod sends every underlying action through Claude Code's $.tool.call(), including its tool permissions and hooks. It starts no agent session or network service.

Install

Requires Claude Code 2.1.293 or later and Node.js 20 or later on PATH. The checked-in dist/broker.mjs contains the QuickJS runtime, so users do not need npm or a dependency install.

From the AITuning checkout, register the separate local marketplace and install the mod:

claude plugin marketplace add ./mods
claude plugin install code-mode@aituning-mods

Restart Claude Code or run /reload-plugins in an existing session. For a single development session, use claude --plugin-dir ./mods/code-mode. If Node is absent, execution fails visibly. Direct tools remain available.

Call the tool

Claude sees mcp__code-mode__execute with one argument, code. The string is the body of an async JavaScript function:

const read = await tools.call('Read', { file_path: 'data.json' });
const data = JSON.parse(read.result.file.content);
return { count: data.length };

The guest receives tools.call(name, args), tools.list(), and emit(value). Calls return the host's structured result and model-facing text; inspect the actual tool result shape. text can contain line numbers and truncation. Read explicit ranges when the source exceeds the tool's limits. An implicit partial text-file read is reported as an error rather than a complete data source.

tools.list() returns currently available names and descriptions. It does not invent input schemas. Use the existing tool declaration or direct ToolSearch before unfamiliar calls. Deferred tools become usable when they appear in the session's tool list. Unknown tools and recursive code-mode calls are refused.

Await calls and return only the requested fields. Mutations take effect immediately. A failed script does not roll them back, and neither the runtime nor the transport retries them.

Use direct tools for single simple calls, images, interactive tools, subagents, or capabilities this JSON bridge cannot preserve. The global routing rule leaves those routes available.

Parallel reads

Use Promise.all for independent built-in Read, Grep, or Glob calls. The host dispatches contiguous pending reads in groups of up to four:

const paths = ['first.json', 'second.json'];
const results = await Promise.all(paths.map(file_path =>
  tools.call('Read', { file_path })
));
return results.map(result => JSON.parse(result.result.file.content).id);

Await a result before computing a dependent request. Await the whole read group before using its results in a mutation. Promise.all preserves input order when calls finish out of order. Parallel reads do not provide a consistent snapshot of files that another process changes.

Every other tool is a sequential barrier, including Write, Edit, Bash, and MCP tools. A barrier waits for the preceding group and finishes before the next begins. Read-only descriptions, annotations, and guest flags cannot change this classification. Each concurrent action still passes through the normal $.tool.call() permission and hook path.

The host waits for all started siblings before advancing or cleaning up, including when a transport call fails. Cancellation or expiry prevents another group from starting. Already-running tools retain Claude's cancellation behavior. A denial remains visible even when a sibling succeeds or the guest catches the error. Barriers enforce ordering, not rollback or conditional execution: do not queue a mutation alongside reads it depends on.

The host also checks returned receipt IDs and statuses against its own action records. A mailbox result cannot erase a host-observed denial or turn it into success. Existing reply files are refused before writing, with no action replay.

Results and limits

The result is JSON with ok, value, output, and a calls receipt. Failures include error. A caught denial or tool error remains visible in the receipt and makes ok false. On host failure, receipts identify actions already completed. A cancelled or expired call is not evidence that an in-flight mutation was undone.

ResourceLimit
Script16,384 characters
Calls32 per script; up to four independent built-in reads at once; other tools sequential
Tool arguments65,536 characters per request
Tool result2 MiB per reply; larger results fail visibly
Guest heap32 MiB
Guest deadline60 seconds, including time waiting for tools
Returned guest output16,000 characters and 100 emitted items

The guest has no host filesystem, Node APIs, environment, network, timers, imports, or persistent state. It can reach outside only through session tools. The trusted Node broker exchanges JSON through a private temporary mailbox. The host uses the mod SDK filesystem only for these broker control files. Requested user-file reads still use $.tool.call() and its normal permissions. A reply becomes readable only after its complete JSON write and a separate completion marker.

SDK polling sleeps total at most 250 ms per execution, then a blocking helper handles longer waits. This keeps polling within the hook's own execution budget while retaining the 60-second guest deadline. Large final diagnostic receipts also use the original process transport. Initialization and cleanup remain in the native broker, which validates the temporary root, owner nonce, and expiry. Normal completion cleans the mailbox. An abrupt host or process termination can leave a temporary directory named aituning-codemode-*; never delete one belonging to an active run.

The mod's deadline stops guest computation and scheduling. Claude Code retains control of cancellation for an underlying tool already running. Permission prompts consume the deadline; if one expires, check the action's actual state before deciding whether to retry.

Develop and verify

From this folder, install the pinned build dependencies with lifecycle scripts disabled and rebuild the self-contained runtime:

npm ci --ignore-scripts --no-audit --no-fund
npm run build
npm test
claude plugin validate . --strict
claude plugin test .

The source files under scripts/ own runtime behavior; dist/ is a generated distribution artifact. Rebuild it after source changes. The pinned runtime is QuickJS Emscripten 0.32.0; bundled licences are in dist/.

The build preserves the upstream Emscripten loader's URL-probe and memory-growth fallbacks, but logs their caught errors to stderr. It requires exactly two matching upstream handlers and fails if the pinned source changes. Dependencies and the repository's floor guard remain unchanged.

The code-mode guide explains Pi setup, shared routing, and the bounded benchmark summary. This distribution includes offline runtime tests; paid benchmark scripts and private run records are omitted.

API sources

The implementation follows Claude's mod authoring restrictions, mods API, and published types. QuickJS integration follows its primary documentation.

Source 1 files
hooks/register.js 154 lines
1const SELF = 'mcp__code-mode__';
2const RESERVED = ['tool', 'tool_use_id', 'agentId', 'consent', '__proto__', 'constructor', 'prototype'];
3const PARALLEL_READS = new Set(['Read', 'Grep', 'Glob']);
4const READ_CONCURRENCY = 4;
5
6function protocol(result) {
7  if (result.exitCode !== 0) throw new Error('Code-mode helper failed: ' + result.stderr.slice(0, 1000));
8  return JSON.parse(result.stdout);
9}
10
11async function run($, e, next) {
12  if (typeof e.code !== 'string' || !e.code.trim() || e.code.length > 16384) return { result: JSON.stringify({ ok: false, error: 'code must contain 1-16384 characters' }) };
13  const catalog = (await $.tool.list()).filter(t => !t.name.startsWith(SELF)).map(t => ({ name: t.name, description: t.description.slice(0, 400) }));
14  const executable = ['node', $.plugin.root + '/dist/broker.mjs'];
15  const owner = protocol(await $.process.run([...executable, 'init']));
16  const route = [owner.dir, owner.nonce];
17  let job;
18  let settled;
19  let active = true;
20  const completed = new Map();
21  const deadline = (await $.clock.now()) + 60000;
22  try {
23    const running = $.process.run([...executable, 'run', ...route], { stdin: JSON.stringify({ code: e.code, catalog }), timeoutMs: 70000 });
24    // Observe rejection immediately while other broker requests are in flight.
25    settled = running.then(value => { job = { value }; }, error => { job = { error: String(error.message) }; });
26    let sleepMs = 0;
27    const readFrame = async () => {
28      const entries = await $.fs.list(owner.dir);
29      const readJson = async entry => {
30        if (entry.kind !== 'file' || entry.isLink || !Number.isInteger(entry.size) || entry.size < 0 || entry.size > 300000) throw new Error('invalid mailbox frame');
31        const text = await $.fs.read(owner.dir + '/' + entry.name);
32        if (new TextEncoder().encode(text).length > 300000) throw new Error('mailbox frame exceeds limit');
33        return JSON.parse(text);
34      };
35      const done = entries.find(entry => entry.name === 'done.json');
36      // Large diagnostic receipts retain the existing process transport; the
37      // SDK filesystem has a per-file cap that the old transport did not use.
38      if (done && done.size > 300000) return protocol(await $.process.run([...executable, 'next', ...route], { timeoutMs: 3000 }));
39      if (done) return { done: await readJson(done) };
40      const ready = new Set(entries.map(entry => entry.name));
41      const pending = entries.filter(entry => /^request-\d+\.json$/.test(entry.name) && !ready.has(entry.name.replace('request-', 'reply-').replace('.json', '.ready'))).sort((a, b) => Number(a.name.slice(8, -5)) - Number(b.name.slice(8, -5)));
42      if (pending.length > 32) throw new Error('broker call limit exceeded');
43      const requests = [];
44      for (const entry of pending) requests.push(await readJson(entry));
45      return { requests };
46    };
47    const dispatch = async request => {
48      const dispatchNow = await $.clock.now();
49      if (next.signal.aborted || !active || dispatchNow > deadline || job) throw new Error('Code-mode cancelled, expired, or stopped before tool dispatch');
50      if (!Number.isInteger(request.id) || request.id < 1 || request.id > 32) throw new Error('invalid request id');
51      const identity = JSON.stringify(request);
52      if (completed.has(request.id)) {
53        if (completed.get(request.id).identity !== identity) throw new Error('request id reused with different arguments');
54        throw new Error('duplicate request; refusing to repeat an action');
55      }
56      // Reserve before the first action so duplicate in-flight IDs cannot dispatch twice.
57      const record = { identity, tool: request.name, status: 'pending' };
58      completed.set(request.id, record);
59      const args = request.args;
60      let value;
61      if (typeof request.name !== 'string' || request.name.startsWith(SELF) || !catalog.some(t => t.name === request.name)) value = { deny: 'Tool is unavailable or recursive: ' + request.name };
62      else if (!args || typeof args !== 'object' || Array.isArray(args) || Object.keys(args).some(key => RESERVED.includes(key))) value = { deny: 'Tool arguments must be an object without reserved metadata' };
63      else {
64        try { value = await $.tool.call({ ...args, tool: request.name }); }
65        catch (error) { value = { isError: true, text: String(error.message) }; }
66      }
67      const file = value.result && value.result.file;
68      if (request.name === 'Read' && file && Number.isInteger(file.numLines) && Number.isInteger(file.totalLines) && file.numLines < file.totalLines && args.offset === undefined && args.limit === undefined) {
69        value = { ...value, isError: true, text: 'Read returned partial content (' + file.numLines + '/' + file.totalLines + ' lines). Read explicit offset/limit ranges before computing a whole-file answer.' };
70      }
71      if (new TextEncoder().encode(JSON.stringify(value)).length > 2 * 1024 * 1024) value = { isError: true, text: 'Tool output exceeded 2 MiB; request a smaller range or query' };
72      record.status = value.deny ? 'denied' : value.isError ? 'error' : 'ok';
73      // The broker ignores the JSON until the complete write is committed.
74      const reply = owner.dir + '/reply-' + request.id;
75      const existing = await Promise.allSettled(['.json', '.ready'].map(suffix => $.fs.exists(reply + suffix)));
76      for (const check of existing) {
77        if (check.status === 'rejected') throw check.reason;
78        if (check.value) throw new Error('reply already exists; refusing duplicate');
79      }
80      await $.fs.write(reply + '.json', JSON.stringify({ id: request.id, value }));
81      await $.fs.write(reply + '.ready', 'ready');
82    };
83    let answer;
84    while (!answer) {
85      if (next.signal.aborted || (await $.clock.now()) > deadline) throw new Error('Code-mode cancelled or expired; completed effects are not rolled back');
86      let frame = await readFrame();
87      // A process may finish between the directory listing and its completion callback.
88      if (job && !frame.done) frame = await readFrame();
89      if (frame.done) { answer = frame.done; break; }
90      if (job) throw new Error(job.error || 'Code-mode helper exited without a final result');
91      const batch = frame.requests || [];
92      if (!batch.length) {
93        // SDK sleeps count against the hook's own 10s budget. Bound them across
94        // the entire run, then let a blocking helper wait outside that budget.
95        if (sleepMs < 250) { await $.clock.sleep(10); sleepMs += 10; }
96        else {
97          frame = protocol(await $.process.run([...executable, 'next', ...route], { timeoutMs: 3000 }));
98          if (frame.done) { answer = frame.done; break; }
99          batch.push(...(frame.requests || []));
100        }
101      }
102      if (batch.length > 32) throw new Error('broker call limit exceeded');
103      // Only contiguous known reads overlap. Every other action is a full barrier.
104      for (let index = 0; index < batch.length;) {
105        const group = [batch[index++]];
106        if (PARALLEL_READS.has(group[0].name)) {
107          while (index < batch.length && group.length < READ_CONCURRENCY && PARALLEL_READS.has(batch[index].name)) group.push(batch[index++]);
108        }
109        // Settle siblings even on a transport failure before cancellation or cleanup.
110        const outcomes = await Promise.allSettled(group.map(dispatch));
111        const failed = outcomes.find(outcome => outcome.status === 'rejected');
112        if (failed) throw failed.reason;
113      }
114    }
115    await settled;
116    if (job.error || job.value.exitCode !== 0) throw new Error(job.error || 'Code-mode runner failed after execution');
117    // Mailbox data cannot erase an action or denial observed by this host.
118    const receipts = [...completed.entries()].sort((a, b) => a[0] - b[0]);
119    if (!Array.isArray(answer.calls) || answer.calls.length !== receipts.length || answer.calls.some((call, index) => {
120      const [id, record] = receipts[index];
121      return !call || call.id !== id || call.status !== record.status || (call.tool !== undefined && call.tool !== record.tool);
122    }) || (receipts.some(([, record]) => record.status !== 'ok') && answer.ok !== false)) throw new Error('Code-mode broker receipt does not match completed host actions');
123    return { result: JSON.stringify(answer) };
124  } catch (error) {
125    return { result: JSON.stringify({ ok: false, error: String(error.message), calls: [...completed.entries()].sort((a, b) => a[0] - b[0]).map(([id, record]) => ({ id, tool: record.tool, status: record.status })), note: 'Effects are real, are not rolled back, and must not be retried automatically.' }) };
126  } finally {
127    active = false;
128    // A guest deadline stops scheduling. Underlying tools retain host cancellation semantics.
129    if (!job) {
130      try { await $.process.run([...executable, 'cancel', ...route]); }
131      catch (error) { $.ui.log('Code-mode cancellation cleanup failed: ' + String(error.message)); }
132    }
133    if (settled) await settled;
134    if (job) {
135      try { await $.process.run([...executable, 'cleanup', ...route]); }
136      catch (error) { $.ui.log('Code-mode mailbox cleanup failed: ' + String(error.message)); }
137    }
138  }
139}
140
141export function register(on) {
142  on('session.start', async ($, e, next) => {
143    await $.tool.register({
144      name: 'execute', isDeferred: false,
145      description: 'Run JavaScript to compose existing tools and filter large results before they enter context. Use await tools.call("Read", {file_path:"..."}); results have text/result fields. tools.list() lists names/descriptions; use existing tool schemas or ToolSearch for unfamiliar schemas. Return a small JSON value or emit(value). Every underlying call keeps its normal permissions. Use Promise.all for independent Read/Grep/Glob calls (up to 4 at once); await dependencies. All other tools are sequential barriers. Await all calls, and await reads before dependent mutations. No Node, imports, filesystem, network, timers, or persistent state in the guest. Limits: 32 calls, 60s, 32MiB guest memory, 16000 output characters. Effects are real and not rolled back on error. Use direct tools for simple calls or images/interactive capabilities.',
146      inputSchema: { type: 'object', properties: { code: { type: 'string', minLength: 1, maxLength: 16384 } }, required: ['code'], additionalProperties: false },
147    });
148    return next(e);
149  });
150  on('tool.call', { tool: 'mcp__code-mode__execute' }, async ($, e, next) => {
151    return run($, e, next);
152  }).catch(($, e, next) => ({ deny: 'Code-mode failed closed: ' + next.error.message }));
153}
154