This guide covers using the stateful codex app-server transport from Elixir via Codex.AppServer.
The SDK supports two external Codex transports:
- Exec JSONL (default, backwards compatible):
codex exec --json - App-server JSON-RPC (optional):
codex app-server(newline-delimited JSON messages over stdio)
Use app-server when you need upstream v2 APIs that are not exposed via exec JSONL (threads list/archive, skills/models/config APIs, server-driven approvals, etc.).
Prerequisites
- A
codexCLI install that supportscodex app-server(runcodex app-server --help). - Auth via either:
CODEX_API_KEY(orauth.jsonOPENAI_API_KEY), or- a Codex CLI login under
CODEX_HOME(default~/.codex).
The SDK resolves the codex executable via codex_path_override → CODEX_PATH → System.find_executable("codex").
Connect / Disconnect
Codex.AppServer.connect/2 starts a supervised codex app-server subprocess and performs the required initialize → initialized handshake automatically.
If the application supervision tree is unavailable, connect/2 returns {:error, :supervisor_unavailable}.
{:ok, codex_opts} = Codex.Options.new(%{api_key: System.get_env("CODEX_API_KEY")})
{:ok, conn} = Codex.AppServer.connect(codex_opts)
# ... use conn ...
:ok = Codex.AppServer.disconnect(conn)Client identity
You can identify your application in the handshake:
{:ok, conn} =
Codex.AppServer.connect(codex_opts,
client_name: "my_app",
client_title: "My App",
client_version: "1.2.3"
)Use app-server as a transport for threads/turns
To keep your existing Codex.Thread.* usage but switch the underlying transport, set transport: {:app_server, conn} in thread options:
{:ok, conn} = Codex.AppServer.connect(codex_opts)
{:ok, thread} =
Codex.start_thread(codex_opts, %{
transport: {:app_server, conn},
working_directory: "/path/to/project",
ask_for_approval: :untrusted,
sandbox: :workspace_write
})
{:ok, result} = Codex.Thread.run(thread, "List files and summarize what you see")Streaming works the same way:
{:ok, stream} = Codex.Thread.run_streamed(thread, "List the top-level files and summarize them")
Enum.each(stream, &IO.inspect/1)Call app-server v2 APIs directly
App-server enables additional APIs that are not available via exec JSONL. Examples:
{:ok, conn} = Codex.AppServer.connect(codex_opts)
{:ok, %{"data" => skills}} =
Codex.AppServer.skills_list(conn, cwds: ["/path/to/project"], force_reload: true)
{:ok, %{"data" => models}} = Codex.AppServer.model_list(conn, limit: 25)
When you need feature-flag gating or to load the underlying `SKILL.md` contents,
use `Codex.Skills.list/2` and `Codex.Skills.load/2`, which honor `features.skills`.
{:ok, %{"config" => config}} = Codex.AppServer.config_read(conn, include_layers: false)
{:ok, _} = Codex.AppServer.config_write(conn, "features.web_search_request", true)
{:ok, %{"data" => threads, "nextCursor" => cursor}} = Codex.AppServer.thread_list(conn, limit: 10)
{:ok, %{"files" => files}} =
Codex.AppServer.fuzzy_file_search(conn, "readme", roots: ["/path/to/project"])Additional v2 APIs include:
Codex.AppServer.thread_read/3,thread_fork/3,thread_rollback/3,thread_loaded_list/2Codex.AppServer.collaboration_mode_list/1andCodex.AppServer.apps_list/2Codex.AppServer.config_requirements/1andCodex.AppServer.skills_config_write/3
When include_layers: true, config_read/2 returns a layers list. Recent Codex versions encode each layer's name as a tagged union (ConfigLayerSource), for example:
%{
"name" => %{"type" => "user", "file" => "/home/me/.codex/config.toml"},
"version" => "sha256:…",
"config" => %{}
}See Codex.AppServer, Codex.AppServer.Account, and Codex.AppServer.Mcp for the full request surface.
Thread management
Common thread-history operations are exposed via:
Codex.AppServer.thread_list/2(supportssort_keyandarchived)Codex.AppServer.thread_archive/2Codex.AppServer.thread_read/3(with optionalinclude_turns)Codex.AppServer.thread_fork/3andCodex.AppServer.thread_rollback/3Codex.AppServer.thread_loaded_list/2Codex.AppServer.thread_resume/3accepts optionalhistoryandpathoverrides
Removed APIs
thread_compact/2- Removed upstream; compaction is now automatic server-side
Legacy v1 APIs
Older app-server builds only implement the v1 conversation endpoints. Use
Codex.AppServer.V1 for those flows:
{:ok, conn} = Codex.AppServer.connect(codex_opts)
{:ok, convo} = Codex.AppServer.V1.new_conversation(conn, %{})
{:ok, _} = Codex.AppServer.V1.send_user_message(conn, convo["conversationId"], "Hello!")Notifications and server requests (approvals)
App-server is bidirectional: the server can send notifications at any time, and it can also send requests that require a response (approvals).
Subscribe from any process:
:ok = Codex.AppServer.subscribe(conn)Messages arrive as:
- Notifications:
{:codex_notification, method, params} - Server requests:
{:codex_request, id, method, params}
You can filter by thread id and/or method list:
:ok = Codex.AppServer.subscribe(conn,
thread_id: "thr_123",
methods: ["turn/completed", "item/completed", "item/commandExecution/requestApproval"]
)Raw response items and deprecations
When experimental_raw_events is enabled on thread/start or
Codex.AppServer.V1.add_conversation_listener/3, the server emits
rawResponseItem/completed notifications. The SDK maps these to
%Codex.Events.RawResponseItemCompleted{} and parses known item types such as
ghost snapshots and compaction payloads. Deprecation warnings are surfaced as
%Codex.Events.DeprecationNotice{} from deprecationNotice notifications.
Config warnings are surfaced as %Codex.Events.ConfigWarning{} from
configWarning notifications.
Request user input
When the agent calls request_user_input, app-server sends an
item/tool/requestUserInput request. The SDK emits %Codex.Events.RequestUserInput{}.
Respond with a Codex.Protocol.RequestUserInput.Response payload:
response = %Codex.Protocol.RequestUserInput.Response{
answers: %{
"q1" => %Codex.Protocol.RequestUserInput.Answer{answers: ["yes"]}
}
}
:ok = Codex.AppServer.respond(conn, id, Codex.Protocol.RequestUserInput.Response.to_map(response))Manual approval handling (UI loop)
When Codex needs approval for a command or file change during a turn/start, it sends a server request:
item/commandExecution/requestApprovalitem/fileChange/requestApproval
Respond by echoing the request id back with a result payload containing decision.
receive do
{:codex_request, id, "item/commandExecution/requestApproval", _params} ->
:ok = Codex.AppServer.respond(conn, id, %{decision: "accept"})
endSupported decision values include:
"accept""acceptForSession""decline""cancel"%{"acceptWithExecpolicyAmendment" => %{"execpolicyAmendment" => ["git", "status"]}}
Note: request id can be an integer or a string.
Headless auto-approval via Codex.Approvals.Hook
When running turns via Codex.Thread.*, you can auto-respond to app-server approvals using approval_hook on thread options.
Supported hook returns (backwards compatible):
:allow→"accept"{:allow, for_session: true}→"acceptForSession"{:allow, execpolicy_amendment: ["cmd", "arg"]}→"acceptWithExecpolicyAmendment"{:deny, reason}→"decline"
Turn diffs
On app-server, turn/diff/updated provides a unified diff string. The SDK surfaces it on Codex.Events.TurnDiffUpdated.diff.
Skills
Skills require the experimental feature flag to be enabled in your codex config:
# ~/.codex/config.toml
[features]
skills = trueSkills can have one of three scopes: "User", "Repo", or "Public".
Skills caveat
App-server v2 does not support sending UserInput::Skill directly today (the union does not include it). Use skills/list to discover skills and inject content as text if you need an emulation layer.
Sandbox Notes
Under workspace-write sandbox mode, both .git/ and .codex/ directories are automatically marked read-only to prevent privilege escalation.
Troubleshooting
skills/list returns -32600 “unknown variant”
If you see a JSON-RPC error like:
code: -32600message: "Invalid request: unknown variantskills/list..."
your installed codex app-server is running a protocol version that does not implement skills/list yet. Upgrade the Codex CLI and retry.
Working live examples
Runnable scripts (against a real codex install) live under examples/:
examples/live_app_server_basic.exsexamples/live_app_server_streaming.exsexamples/live_app_server_approvals.exsexamples/live_app_server_mcp.exs
Run them with:
mix run examples/live_app_server_basic.exs
mix run examples/live_app_server_streaming.exs "Reply with exactly ok and nothing else."
mix run examples/live_app_server_approvals.exs
mix run examples/live_app_server_mcp.exs