← Blog

Running TypeScript mods outside the Codex process

A status line needs session information, settings and somewhere to draw. Add Git status, account limits and a configuration dialog, and it also needs timers, file reads and network requests. If that code runs inside the agent process, a bug in the status line can crash the agent.

Oh My Diff runs TypeScript mods in separate sandboxed processes. To read files, run programs, save state or draw UI, a mod calls the Rust host. The host checks each call against the permissions the user granted. This lets a mod add a status line without getting unrestricted access to the agent.

This article describes OMD 1.4.0, pinned to Codex rust-v0.166.0. Performance results from earlier versions are labeled as such. OMD is still in beta, and the public API marks which contracts are stable and which surfaces are experimental.

The Codex patch connects the agent to the host, which sends callbacks to the mod runner:

Architecture diagram: Codex core and terminal UI exchange calls with the native extension, which talks to the Rust mod host. The host exchanges requests and host-call replies with a sandboxed V8 runner, which runs the TypeScript feature package. The host also owns state, grants and host operations.

codex-omd loads mods and provides their host operations. codex-omd-extension connects it to Codex. A repository check rejects feature names in native code and prevents the host from depending on Codex core or the TUI. Features belong in TypeScript packages. If a feature needs a new host operation, that operation must also be useful to unrelated mods. Runtime architecture

The rest of this article follows a small mod that stores notes. The model gets an “add note” tool, the user gets a clear command, and the terminal shows the count. The public author guide implements that example with api.tool, api.command, api.state, and a terminal event handler.

Its command can be as small as:

api.command('clear', () => {
  api.state.set([]);
  return { display: 'Notes cleared' };
}, { description: 'Delete every saved note' });

The command asks the host to save an empty notes list and display “Notes cleared.” Its manifest declares the tools, commands, store and terminal capabilities used by the package. The host refuses a registration or call unless the user trusted the required capability. Complete notes example

Checking a callback before and after it runs

Before calling a mod, the host checks its source and permissions. It sends the callback input and state to the runner. As the mod makes host calls, the host checks each one and returns a result. When the callback finishes, the host checks permission again before accepting its state and result.

While a callback waits, the user might cancel it, disable the mod, revoke a grant or replace its source. The runtime can also move to a new generation after a reload. Any of these changes can make the host reject the callback’s final state and result. The host cannot undo effects an earlier host call already caused. Lifetime contract

The runner is a separate executable embedding V8. Mods do not run inside Node, although author tooling and some sidecars use Node. The runner's main loop prepares a context, announces readiness, reads requests, executes the selected callback, and writes a result. A reusable process prepares the next fresh context after the previous callback. That context has never run mod code.

The host keeps the runner process alive to avoid launching it again, but gives ordinary callbacks fresh JavaScript contexts. Module globals therefore do not survive those callbacks; use host state for data that must persist. Render hooks can retain their context while their reuse key matches, so their module variables may survive between renders. Runner implementation, host process handling

macOS sandboxes the runner process. Its JavaScript context exposes the mod API and has no general Node environment. The runner disables string code generation and WebAssembly generation. A watchdog terminates execution when its JavaScript budget expires. The configured V8 old-generation limit is 64 MiB. It caps the JavaScript heap only, and the runner's total resident memory can exceed it. A fatal runner failure ends that process while Codex keeps running.

The host limits each callback to 512 host calls and 1 MiB of combined arguments and results. A separate execution budget covers JavaScript and specified terminal waits. Human confirmations can wait longer, and other operations retain their own timeouts. A result that exceeds the remaining transport budget becomes an error even though the operation has already run. Authors therefore need paging and artifact references for large data, and cannot interpret every failed reply as proof of no effect. Budgets, transport

Retrying state updates without repeating commands

Two Codex processes can still write the same saved mod state, so OMD uses compare-and-swap. Suppose two sessions read a notes list at revision 7. One appends a note and commits revision 8. The other cannot overwrite it using its stale revision. The host reruns that callback against fresh state within its budget.

A retried callback might already have run a command or sent a request. OMD records each host-call result as a receipt and returns that recorded result when the retry makes the same call. The callback’s clock stays tied to the first attempt so timestamps in call arguments still match.

If a retry makes different calls, the host starts it afresh only when every recorded call was read-only. If any call could have caused effects, the host returns state_conflict rather than risk repeating it. State retry semantics

api.state and store.* use ordered writes. They survive a crash in order, but a power failure can lose the most recent commits. Evidence records are fully synced. For selections, focus and other temporary UI data, use revisioned UI state; it does not survive a restart.

Combining mods

Two mods can request the same key, panel or tool interception point. OMD applies them in a fixed order and records conflicts so the result does not depend on which callback finishes last.

Dependencies load before the mods that use them; remaining packages load in alphabetical ID order. An administrator-managed selection overrides a project selection, which overrides a user selection. The host merges terminal presentation fields, but gives exclusive fields to the first mod that claims them. It records later claims as conflicts. If a patch exceeds the combined view’s limits, the host rejects the whole patch. Composition rules

An interceptor can answer a call without passing it to the next handler. It must still run any required managed guard further down the chain. While next waits, the host releases the runtime lock so a nested operation can acquire it. Older optional middleware can be skipped after a wait, with the skip recorded; required interceptors must run or the operation fails. Interception, runtime waits

Caching terminal patches and compiled code

OMD reuses the last terminal patch while its settings, durable state, UI state and terminal input remain unchanged. This avoids calling a runner again on every redraw.

A status line that shows elapsed time must update even when its other inputs stay the same. If a callback reads the clock, OMD reuses its patch for at most one second. A patch returned by a command also lasts at most one second; the next view update then calls the terminal handler. Terminal patch reuse

OMD also caches V8 bytecode, using the module source and runner identity as the key. The runner still compiles the trusted source supplied in the request; the cache cannot supply replacement source. OMD limits the disk cache to 32 MiB and removes older entries first. The runner has a separate 1 MiB compilation cache. OMD disables the persistent code cache when the authority broker or administrator policy pins trust. Cache implementation, protected authority

Protecting grants from other programs

Any process running as your user can change grant files that you own. OMD reports this mode as unprotected same-user authority. Its optional macOS broker moves protected grants, credentials, and anchors to a separate OS account and accepts connections only from authenticated programs whose identities it has pinned. Installing the broker requires administrator rights, but ordinary processes running as your user can no longer rewrite those grants.

An approved program running outside the sandbox still has your user account’s access. The broker protects credentials and grants only, so such a program can still do harm with your account. Mods use credentials through specific host operations and cannot request arbitrary secret values. Broker design and limits

Startup and memory measurements

The How OMD works page reports startup and memory measurements from one Apple M5 Mac, using a scripted model and release builds on Codex 0.165 and 0.166 in October 2026:

Observation Stock Codex OMD+ as installed
Ready to type, approximately 0.3 seconds 0.3 seconds
macOS memory footprint, ten seconds after start not reported 100 MB

Each omd-runner in that OMD+ sample used 7–10 MB, and turning on one of the heavier opt-in mods added a runner of about 8–12 MB. These figures are Activity Monitor-style footprint, which is a different measure from RSS. Programs launched by mods have their own costs. The measurements cover one configuration and do not include current-release latency across terminals or model inference. Measurement source

Earlier experiments measured how reusing a runner process and caching compiled code reduced callback time. During the earlier Node-based implementation, matched isolated callback probes measured a 26.18 ms median for a prebooted one-shot runner, 6.88 ms for process reuse without the explicit compilation cache, and 3.15 ms with it. Those probes excluded Rust host and terminal work and overlapped compilation. The numbers apply to that mechanism in that implementation, and today's V8 executable would need its own benchmark. The separate startup investigation looks at an input timer that seemed unrelated but still affected latency after callback work was improved.

Choosing and testing an extension

Use a skill for a procedure the model should follow, or a plugin or MCP server for additional tools. Running extension code in the agent process would avoid host-call transport costs, but a crash could take down the agent. A private fork can change code beyond OMD’s API; its author must also integrate each upstream release.

OMD’s build scripts apply the native patch to one exact Codex tag, check before-and-after file hashes and verify that applying it twice leaves the same tree. They also generate readable copies of the added crates. Conformance checks compare API declarations with host operations. Offline author scenarios test callback behavior without calling a provider. Testing filesystem access, process isolation, terminal rendering and real providers requires separate runs against those systems.

When writing a mod, keep feature code in TypeScript, save persistent data through the host APIs and request only the permissions it needs. The notes tutorial shows those choices in a complete mod.