# Run Codex headless with codex exec, on real tickets

> Run Codex in an isolated sandbox that never holds your OpenAI key, with CI, review and QA around the run. Codex builders resolved 55 of 60 benchmark missions.

- URL: https://letsship.ai/blog/run-codex-headless
- Author: Önder Ceylan
- Published: 2026-10-15

To run Codex headless on real tickets, add an OpenAI API key to SHIP and set `harness: codex` for the builder or the reviewer in `ship.yml`. Then assign an issue, or run `ship delegate plan --harness codex`. SHIP runs Codex in an isolated sandbox with no terminal attached and no key inside it. Your CI, a reviewer and QA check the work. In SWE-in-a-team, Codex builders resolved 55 of 60 missions.

## Read what codex exec does on its own

OpenAI's [non-interactive mode docs](https://learn.chatgpt.com/docs/non-interactive-mode) cover `codex exec` on its own. The command takes a task prompt as a single argument:

```bash
codex exec "summarize the repository structure and list the top 5 risky areas"
```

Progress streams to `stderr`. Only the final agent message prints to `stdout`, so you can pipe the result to a file or another command. Add `--ephemeral` when you do not want Codex to keep its session files on disk.

By default, `codex exec` runs in a read-only sandbox. `--sandbox workspace-write` allows edits. `--sandbox danger-full-access` grants broader access, and the docs say to use it only in a controlled environment. The older `--full-auto` flag is retired in favor of `--sandbox workspace-write`. `--approve-for-me` routes approval requests through automatic review with the workspace-write sandbox.

For machine-readable output, `--json` turns `stdout` into a JSON Lines stream that captures every event Codex emits, such as `thread.started`, `turn.completed` and `item.*`. The flag `-o`, or `--output-last-message`, writes just the final message to a file. `--output-schema` asks for a final response that matches a JSON Schema you give it.

On authentication, the docs say to set `CODEX_API_KEY` only for the one call that needs it. They warn against a job-level environment variable for `OPENAI_API_KEY` or `CODEX_API_KEY` in a workflow. They call an API key the right default for automation. For CI, they point to the Codex GitHub Action instead of a hand-written CLI step, because the action starts a secure proxy for the key. The GitHub repository's own [exec reference](https://github.com/openai/codex/blob/main/docs/exec.md) adds no separate claim. It points back to the same non-interactive mode page.

That leaves a reader with a command and a set of flags. Three things are still to build: a container for the sandbox, a place for the key, and a check on what the run produced.

SHIP builds that part for you. A Codex stage works in an isolated sandbox. It runs your formatter, linter and tests before it pushes, and opens the pull request ([how a mission runs](https://letsship.ai/docs)). Each sandbox gets a scoped, single-use callback token. Before the agent reports back, SHIP stores the full session, the assembled prompt, the token usage and the cost. Nobody attaches to the run, and that record is what you read afterward.

## Add an OpenAI API key

On the **Connect your AI providers** step, add an OpenAI credential. SHIP checks it with a live call when you save it. A typo or a key from the wrong account fails there, before your first mission ([LLM credentials](https://letsship.ai/docs/getting-started/llm-credentials)).

Every role defaults to the `claude-code` harness on Opus. So an Anthropic credential covers the Planner, the Builder, the Reviewer and QA out of the box. Add the OpenAI key to put Codex on one or more roles, as the next section shows.

The key never enters the sandbox. SHIP starts the agent's container with a placeholder and puts the real credential on the outbound request, outside the container. If SHIP does not replace the placeholder, the provider gets the placeholder and rejects the call.

## Give the sandbox your toolchain

Every agent starts in a sandbox built on the platform base: an isolated container with Node, pnpm, git and the agent CLIs ([Custom sandboxes](https://letsship.ai/docs/configuration/sandboxes)). If that is all your repository needs, skip this step.

If Codex needs more, such as Python, a JDK or a system library, write a custom sandbox. Its definition is a short Dockerfile with no `FROM` line, because it always starts from the platform base:

```dockerfile title=".ship/sandbox.Dockerfile"
RUN export DEBIAN_FRONTEND=noninteractive && apt-get update \
  && apt-get install -y --no-install-recommends python3 python3-venv python3-pip \
  && rm -rf /var/lib/apt/lists/*
ENV UV_LINK_MODE=copy
```

SHIP supports `RUN`, `ENV`, `ARG` and `WORKDIR`. It refuses `COPY`, `ADD` and `USER`, with the line and the reason. It builds the definition once, keeps a snapshot, and starts every mission from that snapshot. No mission pays to install the tools.

This answers the "docker" search behind this page. OpenAI's own pages describe no container image for `codex exec`. SHIP's custom sandbox is a Dockerfile format, not a Docker runtime you manage. You choose where each role runs with `agents.<role>.sandbox` in `ship.yml`, or with a `.ship/sandbox.Dockerfile` in your repository. Use `agents.<role>.size` for more CPU or memory, from `standard-1` up to `standard-4`, which is the default. SHIP reads `ship.yml` and `.ship/sandbox.Dockerfile` from your default branch only, so a pull request cannot change the sandbox that reviews or tests it.

## Put Codex on the builder or the reviewer

With no `ship.yml`, every role runs `claude-code` on `opus`. Add the file at the repository root to put Codex on a role instead:

```yaml title="ship.yml"
# yaml-language-server: $schema=https://letsship.ai/schema/ship.yml.json
version: 1

deployments: {}

agents:
  builder:
    harness: codex
```

The roles you leave out keep their defaults. A model alias, `codex` or `gpt`, follows the newest model of its family. A versioned model id stays exactly where you put it ([Schema](https://letsship.ai/docs/configuration/schema)).

[SWE-in-a-team](https://letsship.ai/blog/swe-in-a-team) has numbers for the Codex builder. Three OpenAI models ran as the `codex` builder on the same 20 tickets, one run each, while the Planner, Reviewer and Tester stayed on `claude-opus-4.8` on `claude-code`. A ticket counts as resolved when the mission reached Ready for Acceptance and hidden tests passed. The figures are recomputed from the [per-run data](https://letsship.ai/data/swe-in-a-team-per-task.csv), API-equivalent at list price.

| Builder model on codex | Resolved | Per resolved ticket | Median mission time |
| ---------------------- | -------- | ------------------- | ------------------- |
| `gpt-5.6-sol`          | 20 of 20 | $3.83               | 16.3 min            |
| `gpt-5.6-terra`        | 19 of 20 | $3.88               | 18.5 min            |
| `gpt-5.6-luna`         | 16 of 20 | $3.08               | 14.0 min            |

Together the three Codex builders resolved 55 of the 60 missions. The study is one trial per cell in one repository. Read the table as directional, not final.

To try Codex on one mission, with no change to the file, set an override:

```bash
ship mission override set SHIP-412 --role reviewer --harness codex
```

## Hand Codex a ticket or a spec

Work reaches Codex from a tracker or from your terminal ([how work reaches SHIP](https://letsship.ai/docs/getting-started/ways-of-working)). On a Linear project, assign the issue to SHIP. On GitHub Issues, comment `@SHIP` from an account with write access to the repository. [Codex on Linear](https://letsship.ai/blog/codex-on-linear-issues) covers the tracker setup.

From a terminal, `ship delegate plan` skips the Planner. The role you name builds from a spec you wrote:

```bash
ship delegate plan --title "Rate-limit the search endpoint" --file docs/plan.md --harness codex
```

If the change already exists on a branch, `ship delegate pr --at reviewer` hands the open pull request to a Codex reviewer instead. [Codex as reviewer for Claude Code](https://letsship.ai/blog/codex-reviewer-for-claude-code) covers that setup.

You follow the mission with no terminal session attached to it. The command `ship status SHIP-412 --json` prints the stage, review state, test state, pull request and agent runs ([Command reference](https://letsship.ai/docs/cli/reference)). Use `@SHIP pause`, `@SHIP resume` and `@SHIP stop` on the issue to control the mission itself.

## Cap what an unattended run can spend

SHIP limits an unattended mission in four ways. All four limits are on by default:

| Limit                                   | Default                     | When it is reached                                                            |
| --------------------------------------- | --------------------------- | ----------------------------------------------------------------------------- |
| Spend per mission                       | $5                          | The mission stops                                                             |
| Credits per mission                     | 2 credits, or 6 agent-hours | The mission pauses until a member of your organization approves 1 more credit |
| Identical failures in a row at one gate | 2                           | The mission shows Needs attention and the pull request stays open             |
| Builder attempts in one mission         | 25                          | The same result, as an outer backstop                                         |

The spend limits are on the [LLM credentials](https://letsship.ai/docs/getting-started/llm-credentials) page. The loop limits sit under `retries` in `ship.yml` ([Retry limits](https://letsship.ai/docs/configuration/retry-limits)). If you raise only `maxRetries`, usually nothing changes, because a stuck mission hits an oscillation limit long before the ceiling.

SHIP prices every stage from the list rates of the model that served each call, not from what the harness reports about itself. Read the per-stage cost of your first few Codex missions before you move a role to a different model.

To run Claude Code instead, see [Run Claude Code headless](https://letsship.ai/blog/run-claude-code-headless).

## Questions

### What is codex exec?

codex exec is the OpenAI Codex CLI command that runs a task from a single prompt argument with no interactive session. It streams progress to stderr. Only the final agent message prints to stdout, so you can pipe it to a file or another command.

### How do I run codex exec in CI?

OpenAI's own docs set CODEX_API_KEY for the one invocation that needs it, never as a job-level environment variable. They recommend the Codex GitHub Action over a hand-written CLI step. In SHIP, no terminal is attached to Codex. A mission starts from an issue or a CLI hand-off, and SHIP stores the full session for you to read.

### Can I run Codex in Docker?

OpenAI's codex exec docs do not describe a Docker image. In SHIP you do not build a container for Codex. To add tools, you write a custom sandbox as a short Dockerfile with no FROM line, and every mission reuses its snapshot.

### Which credential does Codex need in SHIP?

SHIP runs the codex harness on an OpenAI API key. SHIP checks it with a live call when you save it, and never puts it inside the sandbox. An Anthropic credential still covers any role you leave on the claude-code default.

### Which model should the Codex builder use?

On SWE-in-a-team's 20 tickets, the Codex builders gpt-5.6-sol, gpt-5.6-terra and gpt-5.6-luna resolved 20, 19 and 16 tickets. They cost $3.83, $3.88 and $3.08 per resolved ticket, and together resolved 55 of 60.

### How much does a headless Codex mission cost?

SHIP records what every stage of a mission cost, priced from list rates for the model that served each call. A single mission stops at $5 by default, and pauses at 2 credits or 6 agent-hours until a member of your organization approves more.
