Building on the Codex app server
codex app-server is the Codex agent behind a JSON-RPC interface. You start
the process, send it requests ("start a thread", "start a turn"), and it streams
back everything the agent does: reasoning, commands, file changes, questions.
OpenAI documents it in the
Codex developer docs and
explained how it was built in
Unlocking the Codex harness.
We built Orage on it: a web app and a native iPhone app that drive a real Codex running on a computer in the cloud. These are the notes we wish we had on day one. They hold for Codex 0.159.3, the version we pin.
Start it
Two ways to listen:
# One JSON object per line on stdin and stdout
codex app-server --listen stdio://
# A WebSocket, for a client on another machine
codex app-server --listen ws://0.0.0.0:4500 \
--ws-auth capability-token --ws-token-file /path/to/token
Over stdio, each line is one JSON-RPC 2.0 object with the jsonrpc field left
out. Stderr is for diagnostics only. Over WebSocket, the client sends
Authorization: Bearer <token> on the handshake, then one JSON object per text
frame. Error -32001 means the server is overloaded: retry with backoff.
The handshake
Nothing is accepted before initialize and then the initialized
notification:
{"id":1,"method":"initialize","params":{"clientInfo":{"name":"my-client","title":null,"version":"0.1.0"},"capabilities":{"experimentalApi":true}}}
{"method":"initialized"}
After that, model/list works even before you sign in, and account/read
tells you whether you need to.
Get the types instead of guessing
The CLI writes TypeScript types for the whole protocol:
codex app-server generate-ts --experimental --out ./generated
We regenerate them on every Codex upgrade and turn the same output into Swift
for the iPhone app, so both clients break at compile time when the protocol
changes, not in front of a user. One small fix: the generated files import each
other without a file extension, so for Node's type stripping we add .ts to
every relative import.
Signing in from a server
A cloud container has no browser. account/login/start with type
chatgptDeviceCode gives you a code to type on any device, and the login lands
in the container. Your ChatGPT plan then pays for the model; no API key needed.
The server asks questions too
The app server sends requests to the client, not only notifications. Your client
must answer each one with { id, result }, or the turn waits forever:
item/commandExecution/requestApprovalanditem/fileChange/requestApprovalitem/permissions/requestApprovalitem/tool/requestUserInput, when the agent asks you somethingmcpServer/elicitation/requestaccount/chatgptAuthTokens/refresh
One Codex, many screens
A phone, a laptop tab and an iPad should all show the same thread live, and any of them should be able to approve a command. Orage keeps one connection to each app server and puts a small bridge between it and every screen, so ids, approvals and answers are handled in one place:
| Problem | What the bridge does |
|---|---|
Two tabs both send request id 1 |
Rewrites each client's ids on the way in, and back on the way out |
| A server request id could collide with a client id | Gives server requests their own ids, srv:1, srv:2 |
| Three screens show the same approval | Sends it to all of them; the first answer wins and the others get serverRequest/resolved |
| The phone was asleep when the approval came | Re-sends every open approval to a screen that connects later |
Every tab calls initialize |
Answers it from the bridge's own handshake, so the app server sees one client |
Notifications simply fan out to every connected screen.
What bit us
A thread you only read is not loaded. After thread/read, the thread stays
notLoaded and the next turn/start fails with -32600 thread not found. Call
thread/resume before a follow-up on such a thread, and retry once on that
error.
Send the sandbox policy with every turn. A thread resumed after an app
server restart can fall back to its default sandbox, even if it started with
full access. The approval policy alone does not grant file access: never can
still run inside a sandbox.
The same question can come back with a new id. When a client resumes a thread, Codex sends the waiting request again, sometimes under a different id. Treat same method and same params as the same question, and answer every id Codex used.
Close what the turn closed. When turn/completed arrives, resolve the
requests still open for that turn, so no prompt outlives it. An interrupted
turn resolves its own requests; do not answer them twice.
Browsers cannot read a refused WebSocket upgrade. If your bridge checks a
cookie, accept the upgrade and close the socket with your own code (we use
4401) so the web app knows to send the person to sign in.
Remote computers
The same protocol runs over the WebSocket listener on another machine. Orage
gives each task a cloud computer running codex app-server --listen ws://, and
routes every request that carries a threadId to that thread's computer. The
screens never need to know where a thread lives.
Test without a model
We keep a fake app server, a short script that answers the methods the apps use and replays a scripted turn: reasoning, a plan, a command that needs approval, a file change, a two-question prompt, markdown, token usage. The server and web tests run against it in seconds; a separate test drives the real Codex binary.
Questions
What is the Codex app server? The Codex agent as a long-running process that speaks JSON-RPC, so another program (an editor, a desktop app, your own client) can drive it.
Do I need an API key? No. Sign in with ChatGPT through the protocol and your plan pays for the model. An API key works too.
Can it run on a server? Yes. Use the WebSocket listener with a capability token, or stdio behind your own server, and sign in with the device code.
Can many screens share one app server? Orage does it with one connection and a bridge that rewrites ids, fans out notifications and lets the first answer to an approval win, as above.
Updated 2026-10-09