This document describes the local headless API for controlling Codex CLI sessions without opening the Electron window.
The API is intended for personal local integrations, such as another desktop app, a web UI running on localhost, or an automation agent.
cd /path/to/remote-codex
npm run apiDefault base URL:
http://127.0.0.1:4317
The server starts Codex by running codex from PATH.
Environment variables:
CODEX_API_HOST=127.0.0.1
CODEX_API_PORT=4317
CODEX_API_TOKEN=change-me
CODEX_WORKDIR=/path/to/default/project
CODEX_COMMAND=/full/path/to/codex
CODEX_OUTPUT_BUFFER_CHUNKS=5000
REMOTE_CODEX_CONFIG=/path/to/config.json
FEISHU_ENABLED=1
FEISHU_MODE=long_connection
FEISHU_APP_ID=cli_xxx
FEISHU_APP_SECRET=xxxNotes:
CODEX_API_HOSTdefaults to127.0.0.1.- Do not bind to
0.0.0.0unless you understand the security risk. CODEX_API_TOKENis optional. If set, every request must include anAuthorizationheader.CODEX_COMMANDchanges the Codex executable path used by default.- The API does not allow clients to choose arbitrary commands unless the server
is started with
CODEX_API_ALLOW_COMMAND=1. Keep that disabled for normal use. - File-based configuration is loaded from
REMOTE_CODEX_CONFIGor~/.remote-codex.json. LegacyCODEX_SHELL_CONFIGand~/.codex-electron-shell.jsonare still supported. Environment variables override the config file.
Authenticated request header:
Authorization: Bearer change-me
A session is one running PTY process. By default, each session runs:
codex
Each session has:
id: session identifier.cwd: working directory.cols,rows: terminal size.cursor: latest output cursor.exited: whether the process has exited.
Output is returned as ordered chunks. Clients should keep the latest cursor
and request only newer chunks.
Integration plugins are loaded from src/plugins/. The built-in Feishu plugin
can receive remote messages through Feishu long connection mode and forward them
to Codex through the same session manager used by this API.
The Feishu plugin can also register an app through the official SDK one-click flow. The API returns an authorization URL and QR image data URL; after the user authorizes in Feishu, the server saves the returned credentials and restarts the plugin.
GET /healthExample:
curl http://127.0.0.1:4317/healthResponse:
{
"ok": true,
"defaultCommand": "codex",
"defaultCwd": "/home/ubuntu",
"sessions": 0
}GET /sessionsExample:
curl http://127.0.0.1:4317/sessionsResponse:
{
"sessions": [
{
"id": "SESSION_ID",
"command": "codex",
"args": [],
"cwd": "/path/to/project",
"cols": 120,
"rows": 34,
"createdAt": "2026-05-03T12:00:00.000Z",
"exitedAt": null,
"exited": false,
"exit": null,
"cursor": 3
}
]
}GET /pluginsExample:
curl http://127.0.0.1:4317/pluginsResponse:
{
"plugins": [
{
"id": "feishu",
"name": "Feishu",
"description": "Receive remote Codex commands and send notifications through Feishu.",
"modes": ["long_connection", "custom_webhook"],
"enabled": false,
"running": false,
"status": null
}
]
}POST /plugins/:id/actions/:action
Content-Type: application/jsonThe Feishu plugin currently supports test:
curl -X POST http://127.0.0.1:4317/plugins/feishu/actions/test \
-H 'content-type: application/json' \
-d '{"text":"Remote Codex test","receiveId":"oc_xxx","receiveIdType":"chat_id"}'Response:
{
"ok": true
}Start one-click Feishu app registration:
POST /plugins/feishu/connect
Content-Type: application/jsonExample:
curl -X POST http://127.0.0.1:4317/plugins/feishu/connect \
-H 'content-type: application/json' \
-d '{}'Initial response:
{
"status": "starting",
"message": "Preparing Feishu authorization..."
}Poll status:
GET /plugins/feishu/connectWhen the authorization link is ready, the response includes:
{
"status": "waiting",
"url": "https://...",
"qrDataUrl": "data:image/png;base64,...",
"expireAt": "2026-05-25T12:00:00.000Z"
}Open url or render qrDataUrl. After authorization succeeds, status becomes:
{
"status": "complete",
"message": "Feishu connected.",
"appId": "cli_xxx",
"userOpenId": "ou_xxx",
"configPath": "/home/user/.remote-codex.json"
}Cancel the current registration:
DELETE /plugins/feishu/connectPOST /sessions
Content-Type: application/jsonRequest body:
{
"cwd": "/path/to/project",
"cols": 120,
"rows": 34
}Fields:
cwd: optional. Working directory for Codex.cols: optional. Initial terminal columns. Defaults to120.rows: optional. Initial terminal rows. Defaults to34.id: optional. Custom session id.
Example:
curl -X POST http://127.0.0.1:4317/sessions \
-H 'content-type: application/json' \
-d '{"cwd":"/path/to/project","cols":120,"rows":34}'Response:
{
"session": {
"id": "SESSION_ID",
"command": "codex",
"args": [],
"cwd": "/path/to/project",
"cols": 120,
"rows": 34,
"createdAt": "2026-05-03T12:00:00.000Z",
"exitedAt": null,
"exited": false,
"exit": null,
"cursor": 0
}
}GET /sessions/:idExample:
curl http://127.0.0.1:4317/sessions/SESSION_IDResponse:
{
"session": {
"id": "SESSION_ID",
"command": "codex",
"args": [],
"cwd": "/path/to/project",
"cols": 120,
"rows": 34,
"createdAt": "2026-05-03T12:00:00.000Z",
"exitedAt": null,
"exited": false,
"exit": null,
"cursor": 0
}
}POST /sessions/:id/input
Content-Type: application/jsonRequest body:
{
"data": "your input\n"
}Important:
- Include
\nwhen you want to press Enter. - For interactive terminal control, send raw terminal input strings.
Example:
curl -X POST http://127.0.0.1:4317/sessions/SESSION_ID/input \
-H 'content-type: application/json' \
-d '{"data":"hello\n"}'Response:
{
"ok": true
}GET /sessions/:id/output?cursor=0Example:
curl 'http://127.0.0.1:4317/sessions/SESSION_ID/output?cursor=0'Response:
{
"cursor": 2,
"chunks": [
{
"cursor": 1,
"data": "Codex output...",
"at": "2026-05-03T12:00:01.000Z"
},
{
"cursor": 2,
"data": "More output...",
"at": "2026-05-03T12:00:02.000Z"
}
],
"exited": false,
"exit": null
}Client polling pattern:
- Start with
cursor=0. - Append all returned
chunks[*].datato your terminal or log. - Store the response
cursor. - Next request uses that cursor.
Example loop logic:
let cursor = 0;
async function poll(sessionId) {
const res = await fetch(
`http://127.0.0.1:4317/sessions/${sessionId}/output?cursor=${cursor}`
);
const body = await res.json();
cursor = body.cursor;
for (const chunk of body.chunks) {
process.stdout.write(chunk.data);
}
}POST /sessions/:id/resize
Content-Type: application/jsonRequest body:
{
"cols": 100,
"rows": 30
}Example:
curl -X POST http://127.0.0.1:4317/sessions/SESSION_ID/resize \
-H 'content-type: application/json' \
-d '{"cols":100,"rows":30}'Response:
{
"session": {
"id": "SESSION_ID",
"cols": 100,
"rows": 30,
"exited": false,
"cursor": 2
}
}The real response includes the full session object.
DELETE /sessions/:idExample:
curl -X DELETE http://127.0.0.1:4317/sessions/SESSION_IDResponse:
{
"session": {
"id": "SESSION_ID",
"exited": false,
"cursor": 2
}
}The real response includes the full session object. The session is removed from the manager immediately after sending the kill signal.
const baseUrl = 'http://127.0.0.1:4317';
async function createSession(cwd) {
const res = await fetch(`${baseUrl}/sessions`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ cwd, cols: 120, rows: 34 })
});
const body = await res.json();
return body.session.id;
}
async function sendInput(sessionId, data) {
await fetch(`${baseUrl}/sessions/${sessionId}/input`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ data })
});
}
async function readOutput(sessionId, cursor = 0) {
const res = await fetch(`${baseUrl}/sessions/${sessionId}/output?cursor=${cursor}`);
return res.json();
}
const sessionId = await createSession('/path/to/project');
await sendInput(sessionId, 'hello\n');
const output = await readOutput(sessionId, 0);
console.log(output);Errors are JSON:
{
"error": "Unknown session: SESSION_ID"
}Common status codes:
400: bad input.401: missing or invalid token.404: session or route not found.500: server or PTY error.
- Keep the server bound to
127.0.0.1for personal use. - Set
CODEX_API_TOKENif another local app or browser page will call it. - Do not expose this API to a LAN or the public internet.
- The API controls an interactive shell-like process. Treat it as trusted local automation only.
- Remote plugins should keep
allowedOpenIds,allowedChatIds, andcodex.allowedWorkdirsrestricted. WithoutallowedWorkdirs, remote users can only start Codex in the configured default working directory.