Overview

Zodex

A remote coding MCP server that gives ChatGPT a real Linux workspace with the three tools GPT models already know how to use well: exec_command, write_stdin, and apply_patch. It is backed by a hard permissions split between the operator CLI and the agent CLI.

RustCLIMCP
Coding AgentsVPS
Zodex

I built Zodex to get more mileage out of OpenAI’s frontier coding models. Paid ChatGPT plans come with rate limits so generous on the best models that I’ve never actually hit one; Codex locally throttles me constantly, and the two limits are counted separately. Give ChatGPT the right MCP tools and it becomes a coding agent with near-unlimited runway on the best available model, driving a real Linux machine: not a sandbox, not a code interpreter, but an actual workspace.

Handing a frontier model a real machine raises an obvious question: what stops it from doing something it shouldn’t? Most of the engineering in Zodex is my answer to that. The tool surface is deliberately tiny, GitHub write access comes in graduated modes I open and close from outside, and the permission boundary is enforced by which binaries exist on which machine rather than by runtime checks. The rest of this page walks through those decisions.

Just three tools, chosen deliberately

Zodex exposes exactly three MCP tools: exec_command, write_stdin, and apply_patch.

These are the same three Codex uses. That’s not a coincidence; it’s the point. OpenAI trains their frontier models inside a Codex harness during reinforcement learning, which means if I want ChatGPT to code well remotely, I need to hand it the tool surface it’s already fluent in. Anything more elaborate fights the model’s instincts.

The apply_patch implementation is literally the Rust crate from OpenAI’s Codex repo, not even the Go port I wrote later (apply-patch-go, which I built with Zodex driving it). Using the original crate keeps patch semantics identical to what the model expects.

Three tools sounds thin, but everything a coding agent actually needs, including long-lived shells, test runs, dev servers, normal git history, PR publishing, and controlled push access, lives underneath that surface. Small API, large capability under it.

Sessions that behave like a real terminal

exec_command isn’t a one-shot subprocess runner. Each command runs in a PTY-backed bash session with a handle the agent can come back to: write_stdin sends input or polls output on the same live process, which is what lets ChatGPT drive a REPL, answer an interactive prompt, or babysit a long test run instead of being limited to fire-and-forget commands. The session model distinguishes running from exited and only hands back a handle while the process is alive, a contract a model can actually reason about.

Two details here came directly from watching agents fail. First, cleanup is process-group-based with graceful-termination windows and force-kill fallbacks, because coding agents constantly spawn nested processes such as dev servers, test watchers, and sub-shells, and a naive kill leaves orphans chewing on the machine. Second, output buffers are bounded and truncate from the front: when a long build scrolls past the limit, the agent keeps the recent output, which is where the error is. It’s a small choice, but it’s the difference between the model seeing the failing assertion and the model seeing ten thousand lines of compiler progress.

Sprites, because coding work is bursty

The first version ran on a VPS. It worked, but it was the wrong shape economically. I was paying 24 hours a day for a machine ChatGPT used for maybe a few hours. Coding is bursty. I wanted a real Linux box that billed by the second, not by the month.

Sprites do exactly that. Same familiar box, same repo checkouts, same shell, but the meter only runs when the agent is doing something. I deploy the Rust MCP server (zodexd) onto the Sprite and expose it through a proxy; ChatGPT connects to the MCP URL, and from that point on it’s talking to a machine that only costs me when it’s earning its keep. The setup flow isn’t “copy a binary and hope” either. The operator CLI installs the runtime, wires credentials, configures git identity, then smoke-tests all of it: workspace writability, reader access, service health, even that the agent user can’t read the publisher’s private key. It anticipates deployment failure modes instead of discovering them later.

What this unlocks day to day

ChatGPT turns out to be an excellent background coding agent. I hand it tasks from my phone or a tab I close, and it works on a real machine while I do something else. Combined with Agentbox, it hands work back and forth with the local agents in goldengoose: ChatGPT pushes code and opens PRs from the Sprite, my laptop-side agents do the last mile that needs production credentials. I wrote up the full workflow, including why ChatGPT’s memory changes the economics of context and what happens when it gets the whole pipeline, in ChatGPT is my background coding agent.

Three ways to give ChatGPT write access

The thing I spent the most design time on is what happens when the agent wants to push code back to GitHub. A single “does it have push access” toggle isn’t the right shape, so there are three write modes, and I move between them as a session earns trust.

1. PRs only: no GitHub credentials on the machine at all. This is the default. ChatGPT commits locally, and when it’s ready to send the change back, it asks a separate publisher daemon on the Sprite to open a pull request. The mechanism is a git bundle: the agent-side command verifies a clean worktree, bundles committed HEAD, and hands it over a local Unix socket to the daemon, which holds the write credentials, pushes a generated branch, and opens the PR. The agent shell never sees a GitHub token. The unit of publication is committed git history, which conveniently also nudges the agent into a clean commit-first workflow. All ChatGPT can do is request a PR; I review and merge from anywhere.

2. Direct push, after I approve a one-time code. When I want to skip the PR ceremony, ChatGPT can request a push grant: a GitHub App device-flow prompt fires, I approve a one-time code, and ChatGPT can push straight to the repo for a bounded window of 30 minutes by default. When the window closes, so does the access. The auto-expiry is what makes this mode safe in practice: if I had to clean up manually, I’d forget, and the boundary would rot. There’s a nice trick underneath: a custom git-remote-zodex helper plus a pushInsteadOf rewrite means the agent just runs ordinary git push, and the write gets routed through the controlled daemon path. The agent keeps its familiar workflow; the policy is enforced underneath it.

3. YOLO mode: full write access, for a window I choose. For repos I own and tasks I trust, approving push after push is friction. An operator grant gives ChatGPT full write access, including cutting releases and pushing to main, scoped to a single repo or all of them, with a TTL (default 2 hours) or open-ended for environments I fully trust. Despite the name, this isn’t a boolean in a config file: grants are structured records with repo scopes, expiry epochs, and merge semantics for overlapping grants, because “temporary scoped access” is exactly the kind of state that becomes a security hole if you model it lazily.

Crucially, only I can enable any of these from my machine. ChatGPT can freely open PRs and request pushes, but the command that opens a write window doesn’t exist on the Sprite at all, which brings me to the design I’m most proud of.

The permissions boundary is a binary, not a check

The reason I’m comfortable running any of the modes above, including full YOLO, is structural: the operator CLI (zodex) and the agent CLI (zodex-agent) are two entirely different binaries with two entirely different command surfaces, and the split is enforced at the binary level, not by a runtime “is this call allowed” check.

zodex lives on my machine, not the Sprite. It’s the only place that can install and upgrade the runtime, rotate keys, deploy the proxy, and tear the whole thing down, and the only place that can open the GitHub write windows above. zodex-agent is what runs in the shell ChatGPT drives, and its surface is deliberately tiny: check whether a grant is open, request one, publish a PR. It can’t inspect the Sprite, can’t change modes, can’t escalate.

That’s the safety property that matters: there is no runtime check to bypass, no config file to edit, no flag to flip. The binary that could escalate isn’t installed on the machine the agent has a shell on. The privileged pieces are further isolated from each other on the Sprite itself. The publisher daemon (zodex-prd) holds the writer app’s private key with ownership the agent user can’t read, its Unix socket permissions are set explicitly, and API keys get redacted out of logs and output. Individually small; together they’re why I trust the boundary.

HTTP API, same core

If you want to bypass ChatGPT and drive the machine yourself, perhaps to script a task, wire Zodex into CI, or build a different agent on top, there’s a direct HTTP API exposing the same three tools without the MCP layer. Both front doors route through one shared service layer rather than two parallel implementations, and the test suite pins that down explicitly: MCP-vs-service and HTTP-vs-service parity tests, plus a test that the PR route is never exposed over the direct API. Parity bugs between duplicate interfaces are the kind of thing you only guard against if you’ve been bitten before; I have.

Those tests are part of a suite of about 170 that leans hard toward operational edges rather than happy paths: stateful shell continuation, concurrent session independence, timeout termination, YOLO expiry and grant merging, publisher socket permissions, clean-worktree enforcement, credential redaction. This is a daemon that holds credentials and runs untrusted-adjacent workloads; the weird edges are the product.

Rust, because apply_patch decided it

The whole system (zodexd, both CLIs, the publisher daemon) is Rust, and the honest reason is that apply_patch made the choice for me: OpenAI’s implementation is already a Rust crate, and I wasn’t going to reimplement patch semantics in another language just to avoid a stack. Once the server had to be Rust, everything else got to share the language.

It turned out to be the right call on its own merits. This is a daemon that streams stdio for long-lived PTY sessions, holds credential grants with TTLs, coordinates with a Cloudflare proxy, and manages a two-app GitHub token dance. It is exactly the kind of program where memory safety and strong types pay for themselves daily. And agents are genuinely excellent at writing Rust now, so the “faster to write” argument for reaching elsewhere mostly isn’t there anymore.

Use and to navigate