Skip to content
AI Primer
release

OpenCode developer explains its custom interpreter's QuickJS trade-offs

OpenCode's developer says its code mode uses a custom interpreter to avoid QuickJS's WebAssembly, worker-thread and serialization costs. A practitioner argues QuickJS suits server agents needing tighter privilege boundaries.

4 min read
OpenCode developer explains its custom interpreter's QuickJS trade-offs
OpenCode developer explains its custom interpreter's QuickJS trade-offs

TL;DR

  • OpenCode uses a custom code-mode interpreter to avoid QuickJS integration costs, according to thdxr's explanation.
  • Large database results make cross-thread serialization a concern, as thdxr explained in a follow-up.
  • QuickJS has an advocate for restricted server agents: 0xblacklight argued that their security model differs from local coding agents with a shell.
  • Nested code mode creates an orchestration problem: jonas wants one code layer at the top of the harness stack.

The package README still calls @opencode-ai/codemode private to the workspace. Its design document describes stripping TypeScript syntax before parsing with Acorn. Meanwhile, Pi's implementation notes describe QuickJS inside Wasm with no network, filesystem or timers.

A bounded JavaScript interpreter

CodeMode gives the model one execute tool backed by an owned tree-walking interpreter, according to its design document. Execution happens without eval.

Hosts define schema-described tools and expose them as an object tree through the package thdxr linked. Within its bounded JavaScript subset, generated programs can:

  • Sequence dependent tool calls, branch and loop.
  • Transform, filter and aggregate intermediate results before returning them to the agent loop.
  • Start independent calls concurrently and await them with Promise.all.
  • Return JSON-safe data and structured diagnostics for program, validation, limit or tool failures.

GeoffreyHuntley described the approach as program synthesis in a reply, saying users are accustomed to defining individual tools and calling them.

QuickJS and large result sets

thdxr listed three costs behind the decision to avoid QuickJS:

  1. WebAssembly complexity.
  2. Worker-thread integration.
  3. Serialization of values moving between threads.

Code-mode scripts tend to compose tool calls, sometimes carrying large result sets from database queries. Sending those results back and forth between threads was the issue thdxr raised in a follow-up.

The database-result example is the sharpest argument for owning the runtime.

Building an interpreter would previously have taken too much effort, but was now easy enough to justify, thdxr said in another reply.

Server-agent privileges

QuickJS suits headless background agents that need restricted privileges and access, 0xblacklight argued. He distinguished that deployment from a coding agent already running locally with a bash shell.

OpenCode's custom interpreter also confines programs to supplied tools. Its design document assigns several boundaries explicitly:

  • Authority: the host chooses available tools; each leaf tool enforces authorization and side-effect policy. Catalog visibility does not grant execution authorization.
  • Ambient access: filesystem, process, environment, network and credential access must go through supplied tools. Modules, imports, arbitrary host globals and prototype mutation are unavailable.
  • Validation: Effect Schemas validate and transform tool inputs and outputs. JSON Schemas only render model-facing signatures; their values still cross the plain-data boundary.
  • Budgets: timeoutMs, maxToolCalls and maxOutputBytes have no package defaults. At most eight tool calls execute concurrently.

A 2,000-token tool catalog

Inline tool signatures have a default budget of 2,000 estimated tokens, calculated as characters divided by four, in the package README. Fixed instructions and namespace summaries sit outside that budget.

  • Every namespace remains listed with its tool count.
  • Complete, JSDoc-annotated signatures are selected round-robin across namespaces, preventing one large namespace from consuming the entire catalog.
  • tools.$codemode.search is always registered, even when every signature fits. Instructions advertise it when the inline catalog is partial.
  • Search returns directly usable JavaScript paths, descriptions and complete TypeScript signatures, with deterministic ranking and pagination.

Nested code mode and operation enumeration

Two compatibility problems appear when code mode meets existing tool interfaces:

  • Nested execution: Pi's Cloudflare MCP examples show a JavaScript program carrying another JavaScript program to an inner executor. Its implementation notes identify double JSON escaping and the inner program's inability to call outer tools as consequences.
  • Operation enumeration: jonas said his API exposes a single run(script) function and cannot enumerate every callable function ahead of time in the same thread. The ChatGPT plugin guidelines he quoted call for each model-callable operation to be exposed individually and prohibit a generic executor from enabling unlisted operations.

Further reading

Discussion across the web

Where this story is being discussed, in original context.

On X· 4 threads
TL;DR1 post
A bounded JavaScript interpreter2 posts
QuickJS and large result sets2 posts
A 2,000-token tool catalog1 post
Share on X