# Codex Headless Login: Device Code, SSH Tunnel, or auth.json

> On a server with no browser, run codex login --device-auth and enter the code on any device. If that is blocked, tunnel port 1455 over SSH or copy auth.json.

- URL: https://blog.laozhang.ai/en/posts/codex-headless-login
- Published: 2026-10-02
- Updated: 2026-10-02
- Author: LaoZhang AI Team (https://blog.laozhang.ai/en/about)
- Category: AI
- Tags: Codex, Codex CLI, Headless login, SSH, Device code

---
On a machine with no browser, sign in to Codex CLI with `codex login --device-auth`. The terminal prints a link and a one-time code, you open the link on any device that has a browser, and nothing has to connect back to the server. The one prerequisite is a switch in your ChatGPT security settings, or in the workspace permissions if an admin manages your account.

When that switch is off and you cannot change it, OpenAI documents two fallbacks: forward the login callback port over SSH and use the normal browser flow, or sign in on a machine that has a browser and copy `~/.codex/auth.json` across. A CI job or any other unattended run is a different case. It should use an API key or a workspace access token, which changes how the usage is billed.

Everything below applies to Codex CLI 0.160.0, the current stable release as of October 2, 2026.

**What was actually run, and what was not.** The mechanics were run on October 2, 2026 on Ubuntu 24.04 with glibc 2.39, OpenSSH 9.6p1, and Codex CLI 0.160.0. Five things were observed there: the short loopback form `127.1` resolved to the IPv4 loopback address; a real `ssh -L` forward with that target carried traffic; the same forward reached the callback listener of a running `codex login`, which answered a bare request with `400 Bad Request` and the body `State mismatch`; `codex login` moved from port 1455 to 1457 when 1455 was occupied; and `codex login --device-auth` printed its prompt with a code that expires in 15 minutes.

One device code attempt was also made against a real personal ChatGPT account, from macOS with the same CLI version, and it stopped in the browser. Device code login was off on that account, and the sign-in page refused to continue before it asked for the code. The setting was then viewed on the account security page and left off.

No sign-in was completed on any of the three routes: the switch was not turned on, the code was never entered, and nothing that appears after you authorize in the browser was seen. Copying `auth.json` to a second machine, token refresh, and revocation were not exercised. musl-based systems such as Alpine were not part of the run, and both ends of the test tunnel were on one host. Steps beyond these observations are attributed to OpenAI's documentation, the 0.160.0 source, or user reports, and are labeled as such.

## Why `codex login` stalls on a server with no browser

Plain `codex login` needs a browser that can reach a port on the same machine where Codex is running. The command starts a small local server on that machine's loopback address, prints a sign-in URL, and waits. After you sign in, in the words of OpenAI's [authentication doc](https://learn.chatgpt.com/docs/auth), "the browser returns your credentials to Codex" by calling that local server.

Over SSH, the browser is on your laptop and the server is listening on the remote host. Your laptop's browser sends the callback to your laptop's own loopback address, where nothing is listening, so the sign-in page finishes and the terminal keeps waiting. The same thing happens in a container, on a devbox, or in WSL when local networking blocks the callback.

The CLI points this out itself. On the Ubuntu test machine, `codex login` announced that it was starting a local login server on the loopback hostname at port 1455, then printed "If your browser did not open, navigate to this URL to authenticate:" and an `auth.openai.com` authorize URL. In the 0.160.0 source, the same message ends with "On a remote or headless machine? Use `codex login --device-auth` instead."

The help output of 0.160.0 lists exactly three login flags and one subcommand:

```text
Usage: codex login [OPTIONS] [COMMAND]

Commands:
  status  Show login status

Options:
      --with-api-key       Read the API key from stdin
      --with-access-token  Read the access token from stdin
      --device-auth
```

That listing is condensed from `codex login --help`, with the generic `-c`, `--enable`, `--disable`, and `-h` options left out. Two things people look for are absent. There is no `--no-browser` flag, although one was proposed in a GitHub issue in December 2025. There is also no flag to choose the callback port. The old `--api-key` flag now exits with a message telling you to pipe the key into `--with-api-key`.

## Which login route fits: device code, SSH tunnel, copied file, or a key

Start with device code and move down the table only when a condition rules a row out.

| Your situation | Route | What it needs | Main limit |
| --- | --- | --- | --- |
| Any other device with a browser, and you can change a ChatGPT security setting | `codex login --device-auth` | Device code login enabled for the account or workspace | Code expires in 15 minutes; labeled beta |
| Device code is disabled, but you reach the host over SSH | Normal `codex login` through `ssh -L` | Port forwarding allowed; a browser on your local machine | Fixed port 1455, with a fallback to 1457 |
| No forwarding and no device code | Sign in elsewhere, copy `auth.json` | File-based credential storage on the source machine | One file per machine; a new login on the source can revoke it |
| No person at the terminal | API key or access token | An OpenAI API key, or a managed workspace | API pricing instead of plan credits, or admin approval |

The first three rows all end in the same state: a ChatGPT sign-in stored on the headless machine, using your plan. The fourth row does not. It is the right choice for automation and the wrong choice if the goal is to use a ChatGPT subscription on a server.

![Decision flow for Codex login on a headless machine: device code first, then an SSH tunnel, then a copied auth.json, with an API key or access token for unattended runs](https://blog.laozhang.ai/posts/en/codex-headless-login/img/login-route-picker.webp)

## Device code login: `codex login --device-auth`, a 15-minute code

Device code is the route OpenAI's doc tells you to prefer on remote and headless machines. The [documented steps](https://learn.chatgpt.com/docs/auth#login-on-headless-devices) are short:

1. Enable device code login in your ChatGPT security settings for a personal account, or in the ChatGPT workspace permissions if you are a workspace admin.
2. On the headless machine, run the command below, or start `codex` and choose **Sign in with Device Code** in the first-run menu.
3. Open the link in a browser, sign in, and enter the one-time code.

```bash
codex login --device-auth
```

Run on Ubuntu 24.04 with 0.160.0, the command printed this prompt. The code is redacted here, and it was never entered, so the flow stopped at this point:

```text
Follow these steps to sign in with ChatGPT using device code authorization:

1. Open this link in your browser and sign in to your account
   https://auth.openai.com/codex/device

2. Enter this one-time code (expires in 15 minutes)
   CODE-REDACTED

Continue only if you started this login in Codex. If a website or another person gave you this code, cancel.
```

The doc only says "your browser". Because the flow never calls back to the server, that browser can be on a phone or a laptop.

The security warning matters. A device code grants a session to whichever terminal requested it, so a code that someone else sends you is a request to sign their machine in to your account. Enter only a code your own terminal printed.

### Where the device code setting lives

For a personal account, the switch is the last item on the account security page at `chatgpt.com/settings/security`, in a group for app security at the bottom. An OpenAI team member gave that location as `chatgpt.com/#settings/Security` in a [GitHub issue reply](https://github.com/openai/codex/issues/2798), along with `chatgpt.com/admin/permissions` for workspace admins. The admin page was not opened.

The label was seen in a Simplified Chinese interface, so the exact English wording may differ from the description here. It names four products and one feature: enable device code login for Codex, Excel, PowerPoint, and Word. Its description says device codes are for signing in to Codex in remote or headless environments, or to ChatGPT inside Excel, PowerPoint, and Word through a browser, and it warns that device codes can be phished and must never be shared. Earlier reports from users in GitHub issues called it "Enable Codex Device Code Authorization".

Check the setting before you start. On the personal account tested on October 2, 2026, it was off. That is one account, not a documented default, and OpenAI's docs do not say which plans have the setting.

With the setting off, the browser stops you before the code. On that account, opening `https://auth.openai.com/codex/device` in a browser already signed in to ChatGPT led to an account chooser and then to a consent page whose Continue button was disabled. No field for the one-time code had appeared yet. The page showed a message saying to enable device code login for Codex, Excel, PowerPoint, and Word in ChatGPT security settings and then run `codex login --device-auth` again. The attempt ended there: the switch was not turned on, the code was never entered, and no login was completed, so the page that follows an enabled setting was not seen.

In a Business or Enterprise workspace, a member cannot turn it on alone. Users whose workspace had it disabled reported the message "Please contact your workspace admin to enable device code authentication". In that case, ask the admin or use one of the two fallbacks below.

### What limits device code login

- **Time.** If the code is not used in time, the source returns `device auth timed out after 15 minutes`. Run the command again to get a new code.
- **Version.** `--device-auth` first shipped in stable 0.44.0 on October 3, 2025, and the public beta was announced on December 8, 2025. On an older CLI, [update Codex CLI](https://blog.laozhang.ai/en/posts/codex-cli-install) first.
- **Account checks.** Open GitHub issues report that a sign-in that demands phone verification still demands it through device code. Device code changes where the browser is, not what the account has to pass.

## SSH port forwarding: tunnel port 1455, and 1457 when the CLI falls back

If you can forward ports, OpenAI's doc says you can use the standard browser flow by tunneling the callback server over SSH. This is the first documented fallback, and it needs a browser on your local machine. From your local machine:

```bash
ssh -L 1455:127.1:1455 user@remote
```

Then, inside that SSH session, run `codex login` and open the printed URL in your local browser. By design, the sign-in page calls port 1455 on your laptop, SSH carries the request to port 1455 on the remote host, and the callback server there hands the credentials to Codex.

![Three lanes showing where the Codex login callback goes: plain login stalls on the laptop's port 1455, SSH forwarding carries it to the remote host, and device code needs no callback](https://blog.laozhang.ai/posts/en/codex-headless-login/img/callback-port-tunnel.webp)

OpenAI's doc writes the same command with the loopback hostname as the forwarding target. `127.1` is the standard short spelling of the IPv4 loopback address, and it names the same destination explicitly. That matches the listener: since 0.158.0, released September 28, 2026, the source binds the callback server to the IPv4 loopback address only. The target is resolved on the remote host.

On Ubuntu 24.04 with glibc 2.39 and OpenSSH 9.6p1, this form was observed up to the callback listener and no further. `127.1` resolved to the IPv4 loopback address. With `codex login` waiting on port 1455, a forward whose target was written `127.1:1455` delivered a bare request for `/auth/callback` to Codex's own listener, which replied `400 Bad Request` with the body `State mismatch`, the same reply it gave without the tunnel. That reply is the listener rejecting a callback that carries no sign-in state, so it shows the forward reaches the right process. It does not show a finished login: no browser authorization was performed, and both ends of the tunnel were on one host, not two machines across a network.

The short form is untested on musl-based systems such as Alpine. If a remote system does not accept the short form, use the command exactly as [OpenAI's doc](https://learn.chatgpt.com/docs/auth#login-on-headless-devices) prints it.

### Forward the port the CLI printed

The docs mention port 1455 only. The source is newer and adds a detail: since 0.128.0, released April 30, 2026, Codex falls back to port 1457 when 1455 is busy. The fallback was observed on Ubuntu with 0.160.0: with another process holding 1455, `codex login` announced its login server on port 1457. According to the source, it fails with `Port … is already in use` only when both are taken. There is no flag or config key to pick another port. The `mcp_oauth_callback_port` setting applies to MCP server sign-ins, not to `codex login`.

`codex login` prints the port it actually started on. It follows that a tunnel for 1455 alone will not receive the callback if the CLI started on 1457. OpenAI's docs do not describe the fallback, so that consequence is an inference. If the printed port is 1457, a reasonable fix is to close the session and reconnect with both ports forwarded. This two-port command is a suggestion that was not run:

```bash
ssh -L 1455:127.1:1455 -L 1457:127.1:1457 user@remote
```

Port 1455 must also be free on your local machine, or `ssh` cannot open the forward. A `codex login` still waiting on your laptop holds that port, so stop it first.

### WSL, VS Code Remote-SSH, and Codespaces

OpenAI staff have attributed many port-in-use and callback failures in WSL and Remote-SSH sessions to local network configuration such as a firewall or VPN, and their recommendation in those threads is device code. OpenAI's docs do not say whether the automatic port forwarding in VS Code Remote-SSH or JetBrains Gateway carries the callback. If an editor-managed forward does not work, open the tunnel yourself with the command above.

## Copy `auth.json` from a machine with a browser: one file, one machine

When neither device code nor forwarding is available, you can sign in on a machine that has a browser and copy the credential cache to the headless one. OpenAI documents three steps: run `codex login` on the machine with a browser, confirm `~/.codex/auth.json` exists, and copy it to `~/.codex/auth.json` on the headless machine.

Over SSH, the documented commands are:

```bash
ssh user@remote 'mkdir -p ~/.codex'
scp ~/.codex/auth.json user@remote:~/.codex/auth.json
```

Without `scp`, one command does both:

```bash
ssh user@remote 'mkdir -p ~/.codex && cat > ~/.codex/auth.json' < ~/.codex/auth.json
```

Into a running Docker container:

```bash
CONTAINER_HOME=$(docker exec MY_CONTAINER printenv HOME)
docker exec MY_CONTAINER mkdir -p "$CONTAINER_HOME/.codex"
docker cp ~/.codex/auth.json MY_CONTAINER:"$CONTAINER_HOME/.codex/auth.json"
```

The doc's warning applies to every one of these: "Treat `~/.codex/auth.json` like a password: it contains access tokens. Don't commit it, paste it into tickets, or share it in chat." Running `chmod 600 ~/.codex/auth.json` on the server afterward is ordinary practice for a file like this. It is not a step in OpenAI's instructions.

### When there is no `auth.json` to copy

This route depends on file-based credential storage. The `cli_auth_credentials_store` setting accepts `file`, `keyring`, `auto`, and `ephemeral`. The docs do not state a default, and in the 0.160.0 source the default is `file` on every operating system. If the file is missing after a successful login, the setting has been changed, possibly by an admin, and the credentials are in the OS credential store or in memory. If you set `CODEX_HOME`, the file lives in that directory instead of `~/.codex`, and the directory must already exist.

### What invalidates a copied `auth.json`

A copied file keeps working only while nothing else uses or revokes the same session. Three boundaries matter.

**One file serves one machine.** OpenAI's [CI/CD auth guide](https://learn.chatgpt.com/docs/auth/ci-cd-auth) says: "Do not share the same file across concurrent jobs or multiple machines." Codex refreshes the tokens during use and writes the new ones back to the file. The guide says this happens when the last refresh is older than about 8 days, and after a 401. Once one machine has refreshed, the other holds a stale refresh token. An OpenAI team member explained in a GitHub issue that a refresh token can be reused only within a limited window, on the order of an hour, and is permanently invalid after that. Two machines can therefore both appear to work for a while before one drops out. For a second server, repeat the whole procedure with a separate login.

**A new login on the source machine can revoke the copy.** Since a change merged on June 12, 2026, every `codex login` first revokes and clears the credentials already stored on that machine, and `codex logout` revokes them too. The docs do not spell out what that means for a copy. The following is an inference from the source that was not tested: after you copy the file to a server, running `codex login` again or `codex logout` on your laptop can end the server's session. If your laptop and the server both need Codex, sign in on the laptop again only when you are prepared to re-copy.

**A revoked session announces itself.** The server reports "Your access token could not be refreshed because your refresh token was revoked. Please log out and sign in again." A session that another machine refreshed first reports the same sentence with "was already used" in place of "was revoked". Either way, the file on that machine is finished and needs to be replaced.

## CI and automation: use an API key, `CODEX_API_KEY`, or an access token

For a job that runs with nobody at the terminal, OpenAI's recommendation is an API key, not a ChatGPT sign-in. No browser is involved:

```bash
printenv OPENAI_API_KEY | codex login --with-api-key
```

This changes who pays. API key usage is billed "at standard API rates" and "uses standard API pricing instead of included ChatGPT plan credits". Features that rely on ChatGPT workspace access or cloud services are limited or unavailable, and Codex cloud requires a ChatGPT sign-in. The trade-off is covered in [Codex API key vs subscription](https://blog.laozhang.ai/en/posts/codex-api-key-vs-subscription).

For a single non-interactive run, you can skip the stored login. The docs state that `CODEX_API_KEY` works with `codex exec`, `codex review`, the TypeScript SDK, and `codex exec-server --remote`. Two details prevent a confusing failure. Interactive `codex` does not read `CODEX_API_KEY`. And `OPENAI_API_KEY` is not listed as a credential variable for `codex exec`, so exporting it alone does not sign the run in. Without either, `codex exec` reuses the saved CLI authentication.

Managed workspaces have a third option: a Codex access token created at `chatgpt.com/admin/access-tokens` after a workspace owner enables the permission. Expiry can be set to 7, 30, 60, or 90 days, and owners and admins can revoke any token.

```bash
printenv CODEX_ACCESS_TOKEN | codex login --with-access-token
```

OpenAI's pages disagree on who can use this. The authentication page says "In ChatGPT Enterprise workspaces", and the [access tokens page](https://learn.chatgpt.com/docs/enterprise/access-tokens) says the feature is "currently supported for ChatGPT Business and Enterprise workspaces". On a Business plan, check the admin console before planning around it.

Keeping a ChatGPT account signed in on a CI runner is documented as an advanced setup: seed `auth.json` only when it is missing, let Codex refresh it during runs, persist the refreshed file for the next job, and never do this on a public repository. The same guide adds that "API keys are still the recommended option" for automation.

## Confirm with `codex login status`, then match the message to a fix

`codex login status` tells you whether the login took effect and which method is active. With no credentials, 0.160.0 on Ubuntu printed `Not logged in` and exited with code 1. The signed-in output was not observed. The docs say the command exits 0 when credentials exist, which makes it usable as a check in a script. In the source, a ChatGPT session reports `Logged in using ChatGPT`, and an API key reports `Logged in using an API key` followed by a masked key.

When login fails, the wording of the message identifies the cause.

| Message or symptom | Seen where | What it means | What to do |
| --- | --- | --- | --- |
| `device auth timed out after 15 minutes` | Terminal, 0.160.0 source | The code was not entered in time | Run `codex login --device-auth` again |
| Consent page with Continue disabled and a message to enable device code login for Codex, Excel, PowerPoint, and Word | Browser, observed on one personal account before any code field appeared; earlier user reports quote "Enable device code authorization for Codex in ChatGPT Security Settings" | The setting is off for a personal account | Turn on the last switch on the account security page, then run `codex login --device-auth` again |
| "Please contact your workspace admin to enable device code authentication" | Reported by users | The workspace has device code disabled | Ask the admin, or use the tunnel or the copied file |
| "device code login is not enabled for this Codex server" | Terminal, 0.160.0 source | The auth server answered the device code request with a 404 | Use the tunnel or the copied file |
| Browser sign-in finishes, terminal keeps waiting | Any remote session | The callback never reached the remote port | Forward the port the CLI printed, or switch to device code |
| `Port … is already in use` | Terminal, 0.160.0 source | Both 1455 and 1457 are taken on that machine | Stop the processes holding them, such as an earlier login attempt |
| "refresh token was revoked" or "was already used" | Terminal, 0.160.0 source | The copied session was revoked or refreshed elsewhere | Sign in again and replace the file; keep one file per machine |
| "ChatGPT login is disabled. Use API key login instead." | Terminal, 0.160.0 source | An admin set `forced_login_method` | Use the method the admin allows |

Three more causes sit outside that table. Behind a corporate TLS proxy or a private root CA, set `CODEX_CA_CERTIFICATE` to the CA bundle before running `codex login`; the docs say it applies to login, HTTPS, and WebSocket connections and falls back to `SSL_CERT_FILE`. A login that ends in a 403 during token exchange is a separate problem, covered in [Codex token exchange failed 403](https://blog.laozhang.ai/en/posts/codex-token-exchange-failed-403). And a 401 that appears after you are already signed in belongs in [Codex 401 Incorrect API key](https://blog.laozhang.ai/en/posts/codex-401-incorrect-api-key).

For anything else, read the login log. Direct `codex login` runs write `codex-login.log` under the log directory, which is `~/.codex/log/codex-login.log` by default.

## Questions about Codex login on remote machines

### Can I use the same Codex login on two servers?

Not by copying one `auth.json` to both. OpenAI's CI/CD guide says not to share the same file across multiple machines, because the first machine to refresh the tokens leaves the other with a stale refresh token. Sign in separately for each server, with device code on each one or a fresh login and copy for each.

### What happens to the server if I run `codex login` again on my laptop?

It can sign the server out if the server's `auth.json` was copied from that laptop. Since June 2026, `codex login` revokes the credentials already stored on the machine before starting a new login. The effect on a copied file is an inference from that source behavior. It is not a documented statement and was not tested. If it happens, the source's message for a revoked refresh token is the one to expect.

### Does Codex CLI have a `--no-browser` flag?

No. As of 0.160.0, `codex login --help` lists only `--with-api-key`, `--with-access-token`, and `--device-auth`. Plain `codex login` already prints the sign-in URL when it cannot open a browser, but that URL is useful on a remote machine only together with an SSH tunnel.

### How do I run Codex CLI in headless mode after logging in?

Use `codex exec`, which runs Codex non-interactively. It reuses the saved CLI authentication by default, so a device code login or a copied `auth.json` on that machine is enough. For runs that should bill an API key instead, set `CODEX_API_KEY` for that command.
