Local Plugins
A local plugin is a TypeScript or JavaScript file that ocx start loads before the proxy begins
serving. It can see every provider send just before it leaves the process and redirect it or add
headers — enough to place a local sidecar (a compression proxy, a recorder) in front of providers
without changing opencodex itself.
Plugins are local to one install. opencodex does not download, update or sign them.
Where plugins live
Section titled “Where plugins live”Put plugin files in plugins/ inside the opencodex home (~/.opencodex/plugins/, or
$OPENCODEX_HOME/plugins/ when that variable is set):
~/.opencodex/plugins/ my-sidecar.ts- Files ending in
.ts,.jsor.mjsare loaded in name order. - Names starting with
.or_, and*.d.ts, are ignored — rename a plugin to_my-sidecar.tsto switch it off. - The directory is optional. Without it nothing is loaded.
- A plugin runs inside the proxy with your credentials, so opencodex refuses a plugin file or a
plugins/directory that is owned by another user or writable by group or others, and refuses symbolic links. Every directory aboveplugins/, up to/, must also be owned by you or root and not writable by group or others, unless it is sticky like/tmp. Fix permissions withchmod go-w ~/.opencodex/plugins ~/.opencodex/plugins/*; on systems whose default umask is002, check the parent directories too. On macOS, an ACL grant to another user or group that can write, delete, change permissions, or add/remove path entries blocks loading, even if the mode is0600; inspect the path itself with/bin/ls -lebd -- <path>. Read-only, deny, inheritance-only, and grants only to the path owner or running user do not block loading. ACL display names such asrootor0are not treated as numeric UID proof. On Linux, extended ACLs are checked whengetfaclis installed. Without it, only owner and mode bits are verified. - On Windows automatic plugin loading is disabled until an ACL trust check is available.
Restart the proxy after adding, changing or removing a plugin (ocx service restart, or stop and
start ocx start). Each loaded plugin prints a Plugin loaded: <name> line at startup; a skipped
plugin prints a bounded reason category. Raw plugin exception text is never printed automatically.
To start once without plugins, set OCX_PLUGINS=0.
Writing a plugin
Section titled “Writing a plugin”A plugin default-exports an object with an optional name and a setup function. setup receives a
context. An asynchronous setup has five seconds to finish; plugins run in the proxy’s own thread, so
a setup that blocks synchronously cannot be interrupted and delays startup until it returns. A
setup that times out is not stopped either: servers or timers it already started keep running, so
start long-lived resources only after the work that can fail.
interface UpstreamTarget { url: string; // absolute upstream URL; assign a new one to redirect headers: Headers; // outbound headers, including credentials — never log them readonly transport: "http" | "websocket";}
export default { name: "my-sidecar", setup(ctx: { log(message: string): void; registerUpstreamRewriter(rewrite: (target: UpstreamTarget) => void): void; onShutdown(teardown: () => void): void; }) { ctx.registerUpstreamRewriter(target => { const upstream = new URL(target.url); if (!upstream.pathname.endsWith("/chat/completions")) return; target.url = `http://127.0.0.1:9000${upstream.pathname}${upstream.search}`; target.headers.set("x-original-origin", upstream.origin); }); },};The context also carries name, configDir (the opencodex home) and pluginDir.
Plugins cannot import opencodex modules — in the packaged binary they are not on disk. Declare the small interfaces you need locally, as above.
How rewrites behave
Section titled “How rewrites behave”- The rewriter runs synchronously on every provider send over HTTP and on the Codex WebSocket connection, after opencodex has picked the transport. Keep it fast; do network checks (health probes) in the background and read a cached result in the rewriter.
- A send redirected to a loopback address (
127.0.0.1,::1,localhost) connects directly, over HTTP and over the Codex WebSocket, ignoring provider proxies andHTTP_PROXY: a proxy elsewhere cannot reach this machine’s loopback. Any other destination follows the normal egress settings (includingNO_PROXY), evaluated against the rewritten URL, on both transports. - The Codex WebSocket rewriter runs for every turn, before an idle pooled socket is reused, and a
socket is only reused for the same destination and the same rewritten headers, apart from the two
per-turn headers
x-codex-turn-stateandx-codex-turn-metadata. Those travel inside each request frame and may differ between exchanges on one socket; changes a rewriter makes to them are discarded. A plugin that starts or stops redirecting takes effect on the next turn. - It runs after opencodex has chosen the provider, account and route, so it does not change routing, account selection, retries or request logs.
- A redirected send goes to the host you chose. That host sees the request exactly as the provider would, credentials included.
- If a rewriter throws, opencodex undoes that rewriter’s edits to the send and disables it for the
rest of the process. Edits made by rewriters that ran before it are kept, so the send goes out as
those left it (unmodified when it is the only plugin). If
setupthrows or times out, the plugin is skipped, anything it registered is removed, and later registration attempts from it are ignored; other plugins and the proxy start normally. - A plugin directory that exists but cannot be read (for example, wrong permissions) is reported at startup rather than treated as empty.

