Skip to main content
Glama

cc-codemod

Code mode for Claude Code. The mod adds one tool, run, that takes a JavaScript function body. Inside it, every tool Claude has is an async function: tools.Bash, tools.Read, tools.Edit, and every MCP tool. The script fans out calls, filters what comes back, and returns only the answer. The raw output of the nested calls never enters the context.

const files = ['api.ts', 'db.ts', 'auth.ts']
const reads = await Promise.all(files.map(f => tools.Read({ file_path: `/repo/src/${f}` })))
return files.map((f, i) => `${f}: ${reads[i].text.split('\n').filter(l => l.includes('TODO')).length} TODOs`)

The script runs on opencode's code mode interpreter, a pure TypeScript interpreter for a JavaScript subset. There's no eval, no WASM and no Node, so it works inside Claude Code's mod runtime. Nested calls go through $.tool.call, which means the usual hooks and permission prompts apply to each one. A Read inside a script counts as a read for Edit's read-before-edit check.

Install

At the Claude Code prompt:

/plugin install cc-codemod --marketplace darjss/cc-codemod

Answer y to add the marketplace and pick a scope. From a shell, claude plugin marketplace add darjss/cc-codemod then claude plugin install cc-codemod@cc-codemod does the same. The tool shows up as mcp__cc-codemod__run.

Related MCP server: code2mcp

What a call returns

  • The script's return value, as a string or indented JSON.

  • console.log lines, in a <console> block.

  • Interpreter warnings, and a note if output hit the 100 KB cap.

  • A footer like [4 tool calls · 3 started early · saved pkg, log]. If the script returned a large nested output unchanged, the footer says so and asks the model to slice it next time.

Each nested call resolves to { text, isError, result }. text is what the direct call would have shown, and result is the tool's structured output. A failed or denied call resolves with isError: true and does not throw.

Limits are 200 nested calls and 100 KB of output per run. The language has no classes, imports, timers or fetch.

Calls that start early

Models write await after await even when the calls don't depend on each other. Before a run, the mod parses the script with acorn and lists the tool calls of its top-level statements whose arguments are plain literals. It asks core about each one with $.tool.check, and every call that core holds read-only and doesn't deny starts at once. When the script reaches the call, it gets the result that's already running.

const pkg = await tools.Read({ file_path: '/repo/package.json' }) // starts at once
const log = await tools.Bash({ command: 'git log --oneline -5' }) // starts at once
await tools.Edit({ file_path: '/repo/a.ts', old_string: 'x', new_string: 'y' }) // a write: the plan ends here
const after = await tools.Bash({ command: 'git diff --stat' }) // runs in its place, after the Edit

The plan ends at the first call that isn't a read, at any command that waits (sleep, until, while, wait, watch, timeout), and at any call of a function the script defined. A call inside a branch, loop or callback never starts early. Core answers ask for most Bash reads. Auto mode's classifier settles those, and in other modes the prompt shows up a little sooner than it otherwise would. The idea comes from OC++'s speculation.

Core judges a shell line as a whole, and it won't vouch for a few kinds of reads. When it declines, the mod splits the line on &&, ||, ; and | with shell-quote. The line still counts as a read if every simple command in it is one of these:

  • a command core holds read-only on its own. $HOME is filled in first, since core rejects variables

  • gh pr|run|issue|repo view/list/checks/diff/status, gh search, or gh api without -X, -f, -F or --input

  • npm view|info|ls|outdated, pnpm ls|why, bun pm ls

  • git in another directory (git -C X, cd X && git ...) when X is under ~/dev or ~/.t3/worktrees. Core refuses git elsewhere because a repo's own config can run commands. Repos under those roots are ones you cloned or made, so they're treated like the working directory, and a clone anywhere else still stops the plan.

Any other variable, a substitution, a redirect other than 2>&1 or 2>/dev/null, or a subshell stops the plan.

Values that carry over

When a run succeeds, its top-level const and let values that are JSON go into the session's $.state. A later run that names one gets it back, so the model can fetch once and filter again without re-reading. A value over 100 KB isn't saved, and the notebook keeps the newest values up to 1 MB. A run that fails saves nothing. This is a small version of OC++'s durable notebook: plain data only, and it's gone when the session ends.

When it helps

The tool is optional. Claude picks it when a step needs three or more independent calls, or when it only wants a slice of a big output. I tried forcing every Bash and Read through it, and it made things worse.

Run

Tokens

Tool calls

Research task, direct tools

2.62M

21

Research task, all calls forced through run

3.45M

33

Small coding task, direct tools (2 runs)

0.69M, 0.66M

Small coding task, run available (2 runs)

0.80M, 0.89M

Both setups wrote the same code for the coding task. On a single-file edit, the script adds overhead and saves nothing. The win shows up on fan-out reads across many files or repos.

Develop

The toolchain is Vite+. vp installs packages, formats with Oxfmt, lints with Oxlint and bundles with tsdown. tsrs typechecks.

vp install
vp run build   # vp pack: bundles build/runtime.ts into hooks/runtime.js
vp run check   # vp check (format, lint), tsrs typecheck, claude plugin validate

hooks/register.ts is the mod. build/runtime.ts wraps CodeMode.execute and gets bundled, because the mod loader can't resolve npm packages. The pack block in vite.config.ts escapes one import( in acorn's error strings, since the loader refuses any module text containing it. The types under .claude-plugin/types/ are written by Claude Code and aren't committed.

Thanks

  • Kenton Varda and Sunil Pai at Cloudflare came up with code mode in Code Mode: the better way to use MCP, September 2025. They turned MCP tools into a TypeScript API and had the model write code against it in a sandbox. This mod is that idea pointed at Claude Code's own tools.

  • opencode wrote @opencode/codemode, the interpreter this mod runs on. It made code mode possible here, since Claude Code's mod runtime has no eval and no WASM. Its split between what a script returns and what the model sees shaped run's output.

  • pi by Mario Zechner showed code mode working over a coding agent's built-in tools (bash, read, edit), not only MCP servers. That's the part cc-codemod copies.

License

MIT. The bundled runtime includes MIT-licensed code from opencode, Effect and acorn. See THIRD_PARTY_NOTICES.md.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    F
    maintenance
    Enables AI models to dynamically create and execute their own custom tools through a meta-function architecture, supporting JavaScript, Python, and Shell runtimes with sandboxed security and human approval flows.
    5
    10
    MIT
  • F
    license
    A
    quality
    Not graded
    maintenance
    Enables execution of TypeScript code to call MCP tools instead of direct tool calls, reducing token usage by up to 98% while orchestrating complex multi-tool workflows through secure sandboxed code execution.
    1
    -
  • A
    license
    A
    quality
    B
    maintenance
    Enables Claude Code to perform programmatic tool calling by executing Python scripts that interact with multiple MCP servers in a single round-trip. This reduces latency and token consumption by keeping intermediate tool results within the local Python runtime instead of the conversation context.
    1
    MIT