---
title: "Connect the Codex CLI"
description: "Point the OpenAI Codex CLI at shunt and have it load-balanced across a ChatGPT/Codex OAuth account pool."
image: "https://shunt-docs.pages.dev/og.png"
---

> Documentation Index
> Fetch the complete documentation index at: https://shunt-docs.pages.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect the Codex CLI

Every other connection guide routes **Claude Code** to a backend. This one runs the opposite
direction: point the OpenAI **Codex CLI** at shunt and let shunt load-balance it across a pool of
ChatGPT/Codex OAuth accounts. shunt relays the Codex CLI's OpenAI Responses traffic **verbatim** —
no Anthropic translation — so the CLI talks the same wire protocol to shunt that it would talk
directly to `chatgpt.com`.

This is the [Inbound Codex Endpoint](/guides/inbound-codex-endpoint/) in practice; that page is the
behavior spec (failover table, exhaustion semantics, reload rules). The steps below are the
end-to-end connection walkthrough. Codex CLI config keys quoted here come from OpenAI's
[configuration reference](https://learn.chatgpt.com/docs/config-file/config-reference) and
[authentication](https://learn.chatgpt.com/docs/auth) docs.

## 1. Enable the endpoint on shunt

The inbound endpoint is **opt-in**. Add the table and point it at a `chatgpt_oauth` provider (the
default provider name is `codex`):

```toml
# shunt config.toml
[server.codex_endpoint]
provider = "codex"        # must be a chatgpt_oauth provider; "codex" is the built-in default
```

`codex` is a built-in `chatgpt_oauth` provider, so no `[providers.codex]` block is required to enable
the endpoint. To point it at a differently-named provider, declare one as a `[providers.<name>]`
table (see the [configuration reference](/reference/configuration/#providersname)) and set
`provider = "<name>"` above.

```bash
shunt check                # validates the endpoint's provider exists + is chatgpt_oauth
shunt run
```

When `[server.codex_endpoint]` is absent, none of the routes are registered and shunt's default
HTTP surface is unchanged. When present, startup validation **rejects** an unknown `provider` or one
whose `auth` isn't `chatgpt_oauth` — the endpoint injects the operator's Codex bearer, so only a
`chatgpt_oauth` provider qualifies. See the
[configuration reference](/reference/configuration/#servercodex_endpoint-optional) for every key.

## 2. Point the Codex CLI at shunt

The Codex CLI appends `/responses` to whatever base URL it uses (Codex speaks the OpenAI Responses
wire protocol — `wire_api = "responses"` is the only value it supports), so shunt registers three
routes and any client shape below lands on one:

| Codex CLI `~/.codex/config.toml` | shunt route it hits |
| :-- | :-- |
| custom provider `base_url = ".../v1"` | `POST /v1/responses` |
| `openai_base_url = ".../v1"` | `POST /v1/responses` |
| `chatgpt_base_url = ".../backend-api/codex"` | `POST /backend-api/codex/responses` |

**Recommended — a custom model provider.** With `requires_openai_auth = false` it is unauthenticated
from the CLI's view, so it needs **no local `codex login`** at all — shunt supplies the account — and
it supports either way of presenting the shunt token (step 3):

```toml
# ~/.codex/config.toml
model_provider = "shunt"          # select it as the active provider
model = "gpt-5.6-sol"             # an entitled slug — see step 5

[model_providers.shunt]
name = "shunt"
base_url = "http://127.0.0.1:3001/v1"
wire_api = "responses"            # the only supported value; also the default
requires_openai_auth = false      # shunt handles auth; the CLI needs no ChatGPT/API login here
```

**Simpler — override a base URL** (no provider block):

```toml
# ~/.codex/config.toml — pick one
openai_base_url  = "http://127.0.0.1:3001/v1"                 # + OPENAI_API_KEY = your shunt token
chatgpt_base_url = "http://127.0.0.1:3001/backend-api/codex"  # loopback only (ChatGPT login mode)
```

`openai_base_url` + `OPENAI_API_KEY=<shunt-token>` is the LiteLLM/llmgateway idiom — the CLI sends
the token as `Authorization: Bearer`, which shunt accepts (step 3), so this shape works **even with
`[server.auth]`**. `chatgpt_base_url` keeps the CLI in ChatGPT-login mode and sends its *own* ChatGPT
token, which shunt can't accept as a client token — use it **only on an ungated (loopback) shunt**.
Both run the CLI in its own OpenAI/ChatGPT auth mode, so both need a local `codex login`; the custom
provider above avoids even that.

> **Set the base URL in config.toml, not via the OPENAI_BASE_URL env var**
>
> Put `openai_base_url` in `~/.codex/config.toml` (user-level). The `OPENAI_BASE_URL`
> **environment variable** does not redirect the Codex CLI's Responses **WebSocket**
> transport — the CLI keeps opening `wss://api.openai.com/v1/responses` and bypasses shunt
> entirely, failing with `Incorrect API key` because it presents your shunt token to OpenAI
> directly. Only the config key redirects both the HTTP and WebSocket paths. (The same applies
> to `chatgpt_base_url`, `model_provider`, and `experimental_realtime_ws_base_url`: these are
> user-level `~/.codex/config.toml` keys — the CLI ignores them in a project-local
> `.codex/config.toml`, and there is no environment-variable equivalent.) With `openai_base_url`
> the CLI still *tries* a WebSocket first; shunt has no `wss` route, so the CLI falls back to
> HTTP on its own. The custom `model_provider` above avoids even that round-trip —
> `supports_websockets` defaults off, so it goes straight to HTTP.

The CLI's own local `~/.codex/auth.json` login is **irrelevant to which account answers** — every
request draws an account from shunt's pool. A loopback `base_url` may stay plain `http://`; use
`https://` for anything remote. Do **not** set `supports_websockets = true` on the shunt provider —
this endpoint is HTTP/SSE-only (see below).

> **Isolate it in a profile**
>
> Keep your normal Codex setup untouched by putting the shunt block in a profile file
> `~/.codex/shunt.config.toml` and running `codex --profile shunt`. Profile files use the same
> top-level keys and only need the values that differ from your base config.

## 3. Present the shunt client token (when `[server.auth]` is set)

If shunt has [`[server.auth]`](/guides/shared-gateway/) configured — recommended for anything beyond
loopback — present the shunt client token one of **two** ways; shunt accepts either.

**A. As an OpenAI-style Bearer key** — the LiteLLM/llmgateway idiom. Set the shunt token as the API
key and the CLI sends it as `Authorization: Bearer <shunt-token>`:

```toml
# ~/.codex/config.toml — built-in openai provider (no provider block needed).
# Set the base URL here, NOT via the OPENAI_BASE_URL env var (see the caution in step 2).
openai_base_url = "http://127.0.0.1:3001/v1"
```

```bash
# then present only the token via env — the CLI sends it as Authorization: Bearer
export OPENAI_API_KEY="<shunt-token>"
```

```toml
# or a custom provider — env_key becomes the Bearer
[model_providers.shunt]
base_url = "http://127.0.0.1:3001/v1"
wire_api = "responses"
env_key = "SHUNT_TOKEN"      # reads $SHUNT_TOKEN, sent as Authorization: Bearer
```

**B. As the `x-shunt-token` header** (or whatever `[server.auth].header` names). Only a custom
provider can attach a header; keep the secret out of `config.toml` with `env_http_headers`:

```toml
[model_providers.shunt]
name = "shunt"
base_url = "http://127.0.0.1:3001/v1"
wire_api = "responses"
requires_openai_auth = false
env_http_headers = { "x-shunt-token" = "SHUNT_TOKEN" }   # reads $SHUNT_TOKEN
```

```bash
export SHUNT_TOKEN="<token>"
```

(Or hardcode `http_headers = { "x-shunt-token" = "<token>" }`.) Either way the token is checked
against `[server.auth]`'s list; a missing or wrong token returns `401 authentication_error`. Without
`[server.auth]` at all the endpoint is open to anyone who can reach it — fine for loopback, not for a
shared gateway. The client's bearer/header is used **only** to authenticate to shunt — it is stripped
and never forwarded to the Codex backend.

> **Your credential never reaches the backend — but your identity headers do**
>
> The `Authorization: Bearer` / `x-shunt-token` you present is used **only** to authenticate to shunt
> (step 3) and is then **stripped** — neither it nor any client credential is forwarded upstream. shunt
> swaps in only the pool account's `Authorization` bearer + `chatgpt-account-id`. Everything else the
> CLI sends is forwarded **verbatim**, so your CLI's **own** `version` reaches the backend (not a
> shunt-fixed one) and `minimal_client_version` model gating (step 5) behaves exactly as it would
> against `chatgpt.com`.

## 4. Provision the account pool on shunt

The endpoint reuses the [Codex Multi-Account](/guides/codex-multi-account/#configure-the-pool) pool
unchanged — provision accounts on **shunt's** host, not the CLI's:

```bash
codex login                       # sign in to a ChatGPT account (browser flow)
shunt login codex --name main     # capture it into shunt's store
# Each additional pool account needs its own login + capture (a separate ChatGPT
# account): `codex login` then `shunt login codex --name backup`, and so on.
```

```toml
# shunt config.toml
[[providers.codex.accounts]]
name = "main"

[[providers.codex.accounts]]
name = "backup"     # each declared name must be provisioned above; an unprovisioned
                # account is skipped on failover (its credential fails to resolve)
```

Selection is **session-sticky**: the Codex CLI's own `session-id` request header keys the account,
so one conversation stays on one account for as long as it stays healthy, then fails over (429 →
rotate, 401 → refresh + retry, 5xx → cool down + rotate). A successful pooled response carries an
`x-shunt-account: <name>` header. On a shared gateway with [`[server.auth]`](/reference/configuration/)
enabled, the sticky key is scoped per authenticated client, so different clients that happen to send
the same `session-id` use distinct sticky keys — preventing cross-client sticky-key collisions. (The
keys are distinct; two distinct keys can still hash to the same account.)

With **no** `[[providers.codex.accounts]]` configured and an empty store, the endpoint falls back to
the single default `~/.codex/auth.json` credential — no pool, no failover — so one Codex login works
the moment `[server.codex_endpoint]` is set.

> **Headless provisioning**
>
> `shunt login codex` captures the **file-based** `~/.codex/auth.json`, so on the shunt host keep
> `cli_auth_credentials_store = "file"` (the default) rather than `keyring`. On a server with no
> browser, sign the account in with Codex's device-code flow (`codex login --device-auth`) or complete
> `codex login` on a laptop and copy `~/.codex/auth.json` over to the shunt host — see
> [Login on headless devices](https://learn.chatgpt.com/docs/auth#login-on-headless-devices).

## 5. Pick an entitled model

> **Codex-suffixed slugs are rejected**
>
> The inbound body's `model` is forwarded upstream **verbatim** and the ChatGPT-account backend only
> accepts the slugs your account is **currently entitled** to — it **rejects** `gpt-*-codex` slugs
> (e.g. `gpt-5.2-codex`) with a `400`. If your Codex CLI defaults to a `-codex` model, override it to
> an entitled slug, e.g. in `~/.codex/config.toml`:
>
> ```toml
model = "gpt-5.6-sol"   # entitled slug, not a *-codex one
```
>
> Current entitled slugs are `gpt-5.6-sol` / `-terra` / `-luna` and `gpt-5.5` / `gpt-5.4` /
> `gpt-5.4-mini` / `gpt-5.2` — older accounts may only have the earlier ones. See
> [ChatGPT / Codex → Route a model](/guides/codex/#3-route-a-model-to-codex) for the authoritative
> list. A request naming a model your subscription lacks fails exactly as it would talking to
> `chatgpt.com` directly.

## 6. Verify

A raw Responses request against any of the three routes should relay verbatim and (when pooled)
return an `x-shunt-account` header:

```bash
curl -N -i -X POST http://127.0.0.1:3001/v1/responses \
  -H "content-type: application/json" \
  -H "x-shunt-token: <token>" \
  -d '{"model":"gpt-5.6-sol","input":"say hi","stream":true}'
```

- A `200` with `content-type: text/event-stream` and an `x-shunt-account:` header ⇒ the pool served
  the request and the SSE is relayed unchanged.
- A `401 authentication_error` ⇒ missing/invalid `x-shunt-token` (step 3).
- A `400` naming the model ⇒ the account isn't entitled to that slug (step 5).

Then run the real Codex CLI a turn — it round-trips through the pool with no code change on the CLI
side beyond the base URL. See [Inbound Codex Endpoint](/guides/inbound-codex-endpoint/) for how this
differs from the `/v1/messages` path and [HTTP Endpoints](/reference/endpoints/) for the registered
routes.

## HTTP/SSE only

Even when the target provider has `websocket = true`, this endpoint always uses the HTTP transport,
so leave `supports_websockets` off (its default) on the Codex CLI's shunt provider. The experimental
[Codex WebSocket v2 transport](https://github.com/pleaseai/shunt/blob/main/docs/m11-inbound-codex-endpoint.md)
is out of scope for this endpoint and tracked as a follow-up.

Source: https://shunt-docs.pages.dev/guides/connect-codex-cli/index.mdx
