A practical guide to codex app-server, the bidirectional JSON-RPC interface that the Codex extension for VS Code and other clients use to control the agent. We'll cover launching the server and its transports, initialization, working with threads and turns, action approvals, and secure remote access.
๐ Currency check: verified as of August 24, 2026 against official OpenAI documentation and the app-server README in the openai/codex repository. Part of the API is experimental and changes between versions: before adopting it in production, generate the schema from your installed CLI version (commands are in the protocol section below).
๐ In short: use app-server when you're embedding Codex into your own product and need conversation history, streaming events, and action approvals. For automation and CI, use the Codex SDK instead. For one-off, non-interactive terminal runs, use codex exec.
What is codex app-server
codex app-server is a Codex CLI subcommand that spins up a server implementing a bidirectional JSON-RPC 2.0 protocol (a remote procedure call format over JSON). Through it, a client gets access to the full Codex experience: authentication, conversation history, streaming agent events, sandboxed command execution, and action approvals.
This is the exact protocol used by the Codex extension for VS Code, which makes app-server suitable for building your own IDE integrations, web applications, and desktop clients. The implementation is open source: see the codex-rs/app-server directory in the openai/codex repository. The client language doesn't matter โ you just need to read and write one JSON message per line. TypeScript, Python, Go, Rust, and other languages all work fine.
Which tool to choose: SDK, exec, MCP, or app-server
| Tool | Use case | Why choose it |
|---|---|---|
| Codex SDK | Task automation, CI, scripts | Simpler and more stable, no manual protocol handling |
codex exec | One-off non-interactive terminal run | No server or persistent connection needed |
| Codex as MCP server | Expose Codex's tools to another agent | Standard MCP protocol |
codex app-server | Custom client with history, streaming, and approvals | Full access to the Codex harness โ the same protocol VS Code's extension uses |
Launching and transports
npm install -g @openai/codex
# stdio (default): one line = one JSON message
codex app-server
# WebSocket on a loopback interface
codex app-server --listen ws://127.0.0.1:4500
# Default Unix socket
codex app-server --listen unix://
| Transport | Flag | Format and status |
|---|---|---|
| stdio | --listen stdio:// | JSONL over stdin/stdout; default mode for child processes |
| WebSocket | --listen ws://IP:PORT | One message per text frame; experimental, not supported for production |
| Unix socket | --listen unix:// or unix://PATH | WebSocket connection over a socket with an HTTP Upgrade |
| off | --listen off | No local transport is opened |
The WebSocket listener also answers HTTP health checks: GET /readyz returns 200 OK once the server is accepting connections; GET /healthz returns 200 OK for requests without an Origin header, while requests carrying an Origin header are rejected with 403 Forbidden.
The protocol and message schema
Like MCP, app-server runs on top of JSON-RPC 2.0, but the "jsonrpc": "2.0" header is omitted on the wire.
A request contains method, params, and id:
{ "method": "thread/start", "id": 10, "params": { "model": "gpt-5.4" } }
A response echoes the id and contains result or error:
{ "id": 10, "result": { "thread": { "id": "thr_123" } } }
A notification has no id:
{ "method": "turn/started", "params": { "turn": { "id": "turn_456" } } }
You don't need to hand-write the message schema: the CLI generates TypeScript types or JSON Schema straight from your installed version, and the generated artifacts are guaranteed to match it.
codex app-server generate-ts --out ./schemas
codex app-server generate-json-schema --out ./schemas
๐ก Tip: regenerate the schema after every Codex CLI update. You can check the list of available models and their parameters by calling model/list instead of hardcoding model identifiers in your client.
Lifecycle: initialization, threads, turns
The protocol is built on three primitives:
- thread โ a conversation between a user and the agent, made up of turns
- turn โ a single user request and the agent's work on it
- item โ a unit of input or output: a message, a command run, a file edit, a tool call
Connection lifecycle:
- The client sends
initializewithclientInfo(client name, title, version) and waits for a response. - The client sends an
initializednotification. Before this handshake completes, the server responds to any request with aNot initializederror; a secondinitializeon the same connection returnsAlready initialized. - The client opens a conversation:
thread/startfor a new one,thread/resumeto continue from a saved id, orthread/forkto branch the history. - The client starts a turn:
turn/startwith athreadIdand input items (text, an image URL, or a local file). - During the turn, the client reads a stream of notifications:
item/started, text deltas,item/completed. - You can add a message to an active turn via
turn/steer, or stop it viaturn/interrupt. - The turn ends with a
turn/completednotification carrying a status ofcompleted,interrupted, orfailed.
In initialize.params.capabilities, a client can opt into experimental methods (experimentalApi: true) and disable unneeded notifications by exact method name (optOutNotificationMethods).
โ๏ธ Note: without experimentalApi, the server rejects experimental methods and fields with an error like <descriptor> requires experimentalApi capability. The opt-in is fixed for the lifetime of the connection.
Method map
| Group | Key methods |
|---|---|
| Threads | thread/start, thread/resume, thread/fork, thread/read, thread/list, thread/archive, thread/unarchive, thread/delete, thread/compact/start, thread/goal/set |
| Turns | turn/start, turn/steer, turn/interrupt, review/start |
| Commands and processes | command/exec with write/resize/terminate; process/* โ experimental, runs outside the sandbox |
| Models and flags | model/list, experimentalFeature/list, permissionProfile/list |
| Configuration | config/read, config/value/write, config/batchWrite, configRequirements/read |
| Files | fs/readFile, fs/writeFile, fs/readDirectory, fs/watch, and other fs/* methods |
| Skills and plugins | skills/list, skills/config/write, marketplace/*; plugin/list, plugin/install and related methods are still in development |
| Apps (connectors) | app/list, invoked via a $demo-app marker in input text and a mention item with path app://demo-app |
| Account | account/read, account/login/start (API key, ChatGPT browser flow, device code), account/logout, account/rateLimits/read, account/usage/read |
Events and errors
After starting or resuming a thread, the client continuously reads transport notifications. Key ones include:
thread/started,thread/archived,thread/closed,thread/status/changedโ conversation lifecycleturn/started,turn/completed,turn/diff/updated,turn/plan/updatedโ turn progressitem/startedanditem/completedโ start and finish of each item; read the final item state fromitem/completeditem/agentMessage/delta,item/reasoning/summaryTextDelta,item/commandExecution/outputDeltaโ streaming deltas of text, reasoning, and command outputthread/tokenUsage/updatedโ token consumption for the active thread
When a turn fails, the server sends an error with a codexErrorInfo field and ends the turn with a failed status. Typical values include ContextWindowExceeded, UsageLimitExceeded, HttpConnectionFailed, ResponseStreamDisconnected, and SandboxError. If an HTTP status from an upstream service is available, it arrives in httpStatusCode.
Overload is a special case. When the incoming queue fills up, the server rejects new requests with code -32001 and the message Server overloaded; retry later. Clients should retry with exponential backoff and jitter.
Approvals and sandboxing
Depending on user settings, command execution and file edits may require approval. The server sends the client a request, and the client responds with a decision.
Message sequence for a command:
item/startedwith acommandExecutionitem.- A request:
item/commandExecution/requestApprovalwithitemId,threadId,turnId, and command details. - The client's response:
accept,acceptForSession,decline, orcancel; for commands there's alsoacceptWithExecpolicyAmendmentโ accept the command and simultaneously add a rule to the execpolicy. serverRequest/resolvedconfirms the request has been closed.item/completedwith a final status ofcompleted,failed, ordeclined.
File edits follow the same pattern: item/fileChange/requestApproval with the same set of decisions. If a request includes networkApprovalContext, it's a request for network access to a specific host and protocol โ show a network-access dialog rather than a shell-command preview.
The isolation policy is set via sandboxPolicy when starting a thread or turn: readOnly, workspaceWrite with explicit writableRoots, externalSandbox (if isolation is already handled by your own environment), or dangerFullAccess. The approval policy is set via approvalPolicy.
๐ด Critical: the thread/shellCommand method runs outside the sandbox with full access and does not inherit the thread's policy. Only expose it in your client for commands explicitly initiated by the user. The same applies to the experimental process/* methods.
The built-in request_permissions tool arrives as an item/permissions/requestApproval request. In your response, grant only the requested subset of permissions and explicitly choose the scope: scope: "turn" for a single turn or "session" for the whole session.
Remote access and authorization
You can run app-server on one machine and connect Codex's terminal interface from another:
# on the host
codex app-server --listen ws://127.0.0.1:4500
# on the client
codex --remote ws://127.0.0.1:4500
The --remote option accepts ws://, wss://, and unix:// endpoints. For an unencrypted channel, keep the listener on loopback or tunnel the port over SSH. For a genuinely remote connection, enable authorization and TLS, and pass the token via an environment variable:
export CODEX_REMOTE_TOKEN="$(cat "$HOME/.codex/app-server-token")"
codex --remote wss://remote-host:4500 \
--remote-auth-token-env CODEX_REMOTE_TOKEN
โ ๏ธ Warning: during the gradual rollout period, non-local WebSocket listeners accept connections without authorization by default. Before exposing a port externally, configure --ws-auth: either a capability token via --ws-token-file or --ws-token-sha256, or a signed bearer token via --ws-shared-secret-file. Never pass a raw token as a command-line argument.
Useful scenarios
"Ask Codex" right inside your own product
The task: your team has an internal tool โ an admin panel, editor, or internal portal โ and you want employees to be able to ask Codex a question without leaving it, complete with conversation history, live-typing responses, and allow/deny buttons for the agent's actions.
Prerequisites: Codex CLI installed on the machine running the service, plus a small amount of code in any language โ the protocol only requires reading and writing JSON line by line.
What to do: the service launches codex app-server as a child process, completes the initialize/initialized handshake, opens a thread via thread/start, and sends the user's question via turn/start. The agent's response arrives as item/agentMessage/delta chunks and is displayed immediately in the UI. When the agent asks for permission to run a command or make an edit, the interface receives a standard request and responds with the user's decision.
A minimal Node.js client looks like this:
import { spawn } from "node:child_process";
import readline from "node:readline";
const proc = spawn("codex", ["app-server"], {
stdio: ["pipe", "pipe", "inherit"],
});
const rl = readline.createInterface({ input: proc.stdout });
const send = (message) => {
proc.stdin.write(`${JSON.stringify(message)}\n`);
};
let threadId = null;
rl.on("line", (line) => {
const msg = JSON.parse(line);
if (msg.id === 1 && msg.result?.thread?.id && !threadId) {
threadId = msg.result.thread.id;
send({
method: "turn/start",
id: 2,
params: {
threadId,
input: [{ type: "text", text: "Summarize this repo." }],
},
});
}
});
send({
method: "initialize",
id: 0,
params: {
clientInfo: { name: "my_product", title: "My Product", version: "0.1.0" },
},
});
send({ method: "initialized", params: {} });
send({ method: "thread/start", id: 1, params: { model: "gpt-5.4" } });
Observed result: the user watches the response type out as it's generated; the turn finishes with a turn/completed notification and a completed status, and the assembled text can be read from item/completed.
Limitation: only one initialize per connection is allowed; experimental methods require capabilities.experimentalApi at initialization time.
Codex stays home, you work from a laptop
The task: your projects and configured Codex setup live on a desktop machine โ say, a home Mac mini โ but you've traveled with your laptop and want to keep working in the familiar terminal interface.
What to do: on the home machine, run codex app-server --listen ws://127.0.0.1:4500 and tunnel the port over SSH. On the laptop, connect with codex --remote ws://127.0.0.1:4500. Store the token in a file and pass it via an environment variable, as shown in the remote access section above.
Observed result: the terminal on your laptop looks like a normal Codex session, but commands execute on the home machine; curl -sf http://127.0.0.1:4500/readyz returns 200 OK.
Limitation: the WebSocket transport is experimental; don't expose it externally without --ws-auth and TLS.
Updating Codex CLI without surprises
The task: you've updated Codex CLI and want to know in advance whether the new version has broken your integration.
What to do: after every update, run codex app-server generate-ts --out ./schemas (or generate-json-schema for other languages) and rebuild your client.
Observed result: any mismatches between your client and the new protocol version surface immediately at build time, before deployment.
Limitation: the schema only matches the CLI version it was generated from, so it makes sense to keep the generated files alongside your client code.
Verifying the result
A minimal working scenario to validate your integration:
- Run
codex app-server --listen ws://127.0.0.1:4500. - Run
curl -sf http://127.0.0.1:4500/readyz: a 200 OK response confirms the listener is accepting connections. - Send
initialize, wait for a response containing the server's user-agent, then sendinitialized. - Run
thread/start, followed byturn/startwith a simple text request.
Success looks like a stream of item/* notifications followed by a final turn/completed with a completed status. A Not initialized error means the handshake order was violated.
Limitations and when it doesn't fit
The app-server command and its WebSocket transport are officially experimental and not supported for production workloads. For CI and direct automation, the documentation recommends the Codex SDK โ app-server's protocol is broader and designed specifically for rich client integrations.
The plugin/list, plugin/read, plugin/install, and plugin/uninstall methods are marked as still under development; avoid calling them from production clients.
thread/rollback and the item/fileChange/outputDelta notification are deprecated; use fileChange items and turn/diff/updated for edits instead.
Under overload, expect -32001 errors and retry with exponential backoff and jitter.
This guide is based on official OpenAI documentation and the app-server README, rather than exhaustively testing every method locally. Check experimental flag behavior against your own CLI version.
Official sources
- Codex App Server documentation
- README and source: openai/codex repository,
codex-rs/app-serverdirectory - Codex CLI command reference
- OpenAI's article on App Server architecture
app-server opens the door to building your own interfaces and workflows around Codex. If you're embedding an agent into a product or a team process, it's worth thinking through the integration architecture before the protocol gets locked into your codebase.
