CI Auth & the GitHub Action
Running Codex safely in pipelines
- Be able to choose between API-key and ChatGPT-account auth for a CI job and scope the key to a single `codex exec` invocation
- Be able to seed `~/.codex/auth.json` from a secret with file storage, and avoid the token-refresh footgun on persistent runners
- Be able to authenticate a headless machine using device-auth or an SSH-forwarded localhost callback
- Be able to wire up `openai/codex-action@v1` with the right inputs (prompt, sandbox, model, effort, output-schema)
- Be able to pick a `safety-strategy` that limits credential exposure and pin the action to a stable ref
- Be able to build the auto-fix-CI workflow: trigger on a failed run, let Codex edit with `workspace-write`, and open a PR
Running Codex in CI means handing an autonomous agent a credential and letting it act on its own — so the whole game is leaking nothing and granting the least power that still gets the job done. This lesson covers the two CI auth paths (exec-only `CODEX_API_KEY` vs. seeded ChatGPT `auth.json`), headless login, the official `openai/codex-action@v1` and its safety strategies, and the auto-fix-CI pattern that turns a red build into a pull request.
- 1The mental model: CI hands an agent your keys
- 2Two ways to authenticate in CI
- 3The API-key path: scope it to one command
- 4The ChatGPT-token path: seed auth.json (and the refresh gotcha)
- 5Headless login: device-auth and the port-1455 callback
- 6The official GitHub Action: openai/codex-action@v1
- 7Safety strategies: shrink the blast radius of a leaked key
- 8The auto-fix-CI pattern: red build to pull request
The mental model: CI hands an agent your keys
Interactively, you are the guardrail — Codex pauses and you approve. In CI there is no human in the loop, so two things change at once: the agent runs unattended, and it runs with a long-lived credential sitting in the environment. Get either wrong and a single prompt-injected file or a malicious dependency can read your key and exfiltrate it, or the agent can wander outside the work you intended.
So every decision in this lesson reduces to two questions:
- Which credential, and how narrowly is it scoped? Prefer a key that only one command can see, over one exported process-wide.
- How little power can the run get away with? Least-privilege sandbox, least-privilege OS user, least-privilege GitHub token.
Keep those two levers in mind and the rest is detail. The dossier's one-line summary is worth memorizing: "sandboxing is used to prevent the model from making harmful changes that might be the result of a prompt injection" — and the same instinct applies to the credential itself.
Key insight
Two levers, every time
Every CI-auth choice is either (1) scope the credential so the smallest possible surface can read it, or (2) scope the power — sandbox, OS user, repo token. When in doubt, narrow both.
Watch out
No human approval in CI
The interactive approval prompt that protects you locally does not exist in a pipeline. The sandbox, the OS-level safety strategy, and a tightly-scoped key are your only guardrails — there is no Ctrl+C to hit when something goes wrong.
Two ways to authenticate in CI
Codex offers ChatGPT-account sign-in OR API-key sign-in everywhere, and CI is no exception. The trade-off is billing and simplicity:
| API key (recommended for CI) | ChatGPT account token | |
|---|---|---|
| What you store | An OpenAI API key in a secret | A seeded auth.json (refresh tokens) in a secret |
| Billing | Usage-based API token pricing, no plan allowance | Your plan's included allowance, then credits |
| Setup | Set one env var | Seed a file, set cli_auth_credentials_store="file", manage refresh |
| Best for | Most pipelines; shared/team CI | Reusing an existing ChatGPT plan's quota |
| Sensitivity | Treat like a password | Treat like a password (it auto-refreshes and is rewritten) |
For most CI, the API-key path is simpler and the documented recommendation — there is no file to refresh and no per-runner state. Reach for the ChatGPT token path only when you specifically want CI to draw on a plan's included allowance rather than billing API usage.
Because model names, prices, and per-plan limits change constantly, don't hard-code which is cheaper — check the live pricing page when you decide.
Note
Both share one usage pool under ChatGPT auth
If you go the ChatGPT-token route, remember local messages and cloud tasks share one rolling 5-hour window plus weekly limits — a busy CI job can eat into the same quota you use interactively. The API-key path bills separately. See https://developers.openai.com/codex/pricing.
The API-key path: scope it to one command
There are two relevant env vars, and the distinction is the whole point:
OPENAI_API_KEY— the general OpenAI key, honored broadly.CODEX_API_KEY— honored only bycodex exec(the non-interactive entry point). The docs recommend setting it for the singlecodex execinvocation so that no other code in the job can read it.
That second variable exists specifically for CI safety. The pattern is to set it inline on the command, not export it for the whole job:
# GOOD: the key is only in the environment of this one process
CODEX_API_KEY="$OPENAI_KEY_SECRET" codex exec --json \
--sandbox workspace-write \
"Fix the failing unit tests and explain the change"Contrast that with exporting it job-wide:
# RISKIER: every step (and any tool/dependency it runs) can read the key
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}When Codex runs untrusted code — installing dependencies, running a build script a malicious PR modified — anything in the process environment is readable. Inline-scoping the key shrinks that window to a single command. (OPENAI_API_KEY is still fine when you control the workflow and the repo is trusted; the inline CODEX_API_KEY pattern is the belt-and-suspenders version for untrusted input.)
Tip
Why inline beats job-level env
A job-level env: block is visible to every step and every subprocess they spawn — including dependency install scripts an attacker may control. CODEX_API_KEY=... codex exec ... puts the secret only in that one process's environment.
Watch out
Precedence is unsettled — don't rely on it
The exact precedence when BOTH OPENAI_API_KEY and CODEX_API_KEY are set inside exec is not clearly documented. Set exactly one in a given invocation rather than depending on which wins.
The ChatGPT-token path: seed auth.json (and the refresh gotcha)
To use ChatGPT-account auth in CI, you store a previously-generated auth.json in a secret and write it onto the runner at the start of the job. Two non-obvious requirements make this work:
- The token must land in a plaintext file, not the OS keychain — so you need
cli_auth_credentials_store = "file"in config. - Codex auto-refreshes the token (when it is ~8 days old or a request returns 401) and writes the new token back to
auth.json. That write-back is the gotcha.
Seeding the file:
mkdir -p "$CODEX_HOME"; chmod 700 "$CODEX_HOME"
printf '%s' "$CODEX_AUTH_JSON" > "$CODEX_HOME/auth.json"
chmod 600 "$CODEX_HOME/auth.json"($CODEX_HOME defaults to ~/.codex. The file is plaintext access/refresh tokens — treat it like a password: never log it, never echo it, lock the file mode down.)
The refresh behavior dictates two rules depending on runner type:
| Runner type | What to do |
|---|---|
| Ephemeral (fresh VM each run) | Seed auth.json every run from the secret. The refreshed token is discarded with the VM — fine, but the seed eventually expires, so rotate the secret periodically. |
| Persistent / self-hosted | Seed once, then do NOT overwrite the file each run (you'd clobber the freshly-refreshed token). Use one auth.json per runner — never share across concurrent jobs, or two jobs will fight over the refresh write. |
Watch out
The write-back footgun
Because Codex rewrites auth.json after a refresh, a workflow that blindly re-seeds the file on every run on a persistent runner will overwrite the valid refreshed token with a now-stale seed — and auth breaks days later. Seed-once on persistent runners; re-seed-every-run only on ephemeral ones.
Headless login: device-auth and the port-1455 callback
Sometimes you need to log in on a box with no browser — a headless server, a container, a remote runner you're provisioning. The normal codex login opens a browser for OAuth, which won't work there. Two documented options:
codex login --device-auth— prints a one-time code you enter on another device's browser. The classic headless flow.- SSH-forward the localhost callback. Normal login spins up a local callback listener on port 1455; forward that port over SSH so the OAuth redirect on your laptop reaches the listener on the remote host.
# Forward the OAuth callback port from the remote box to your laptop
ssh -L 1455:localhost:1455 user@remote-host
# then, on the remote host:
codex login # opens the callback on :1455, which you reach locallyOther auth subcommands worth knowing for scripted setup: codex login --with-api-key (key via stdin), codex login --with-access-token (token via stdin), and codex login status (exits 0 when logged in — handy as a CI precondition check). Because these flags evolve, confirm against https://developers.openai.com/codex/auth.
Whichever method you use, the result is the same ~/.codex/auth.json you'd seed directly — device-auth and port-forwarding are just ways to produce that file on a machine without a browser.
Tip
Generate once, store as a secret
You rarely run --device-auth inside CI. Run it once on a trusted machine to produce auth.json, then store that file's contents as a CI secret and seed it on the runner — that keeps the interactive step out of your pipeline entirely.
The official GitHub Action: openai/codex-action@v1
Rather than scripting codex exec yourself, the official openai/codex-action@v1 installs the CLI, starts a Responses-API proxy when you give it an API key, and runs codex exec under a configurable safety strategy. It runs on Linux and macOS runners; Windows requires safety-strategy: unsafe.
The inputs map almost one-to-one onto the exec flags you already know:
| Input | Purpose |
|---|---|
openai-api-key | The API key (from a secret) — wires up auth |
prompt / prompt-file | The task instructions (pick one) |
sandbox | workspace-write | read-only | danger-full-access |
model / effort | Model id and reasoning effort |
output-schema / output-schema-file | Force schema-conforming structured output |
output-file | Also write the final message to a file |
codex-args | Extra raw CLI flags (JSON array or shell string) |
codex-version | Pin a specific CLI release for reproducibility |
safety-strategy | OS-level privilege reduction (next section) |
codex-user, allow-users/allow-bots/allow-bot-users | Who/what may trigger the run |
The Action exposes one output, final-message (the final codex exec message), which you map to a job output for downstream steps. A minimal PR-review job:
name: Codex pull request review
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
codex:
runs-on: ubuntu-latest
permissions:
contents: read # least-privilege token
outputs:
final_message: ${{ steps.run_codex.outputs.final-message }}
steps:
- uses: actions/checkout@v5
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
- name: Run Codex
id: run_codex
uses: openai/codex-action@v1
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
prompt-file: .github/codex/prompts/review.md
output-file: codex-output.mdVolatile note: the recommended/default model and the CLI version rot. Leave model unset to track the default, or pin codex-version deliberately for reproducibility — and check https://developers.openai.com/codex/models and https://github.com/openai/codex/releases.
Tip
Pin @v1 or a SHA — never @main
Use openai/codex-action@v1 (the stable major tag) or a full commit SHA. The cookbook example pins @main for demos, but @main can change under you; production pipelines should pin to a ref they control.
Note
Restrict who can trigger it
allow-users, allow-bots, and allow-bot-users gate who can start the run. On public repos especially, limit triggers to trusted events/users so a forked PR can't make your agent run with your key.
Safety strategies: shrink the blast radius of a leaked key
The safety-strategy input controls OS-level privilege reduction before Codex runs — a layer beneath the sandbox. Its job is to ensure that even if an agent run is compromised, it can't trivially exfiltrate OPENAI_API_KEY or wreck the machine.
| Strategy | What it does | When |
|---|---|---|
drop-sudo | Default. Removes sudo membership before running Codex (Linux/macOS) | The safe default for most jobs |
unprivileged-user | Runs Codex as the account named in codex-user, fixing ownership | When you want stronger isolation than dropping sudo |
read-only | Filesystem viewing only; no network/mutation | Review-only jobs that must not change anything |
unsafe | No privilege reduction | Required on Windows runners; avoid elsewhere |
The guidance is blunt: keep safety-strategy on drop-sudo (or move Codex to an unprivileged user) so a compromised run can't read your key out of the environment or escalate. Only drop to unsafe when the platform forces you to (Windows), and compensate by tightening everything else — sandbox, key scoping, who can trigger.
Notice the defense-in-depth stacking: safety-strategy (OS user/privilege) sits under the sandbox (where Codex may read/write/network), which sits under the approval policy (when it must ask — largely moot in unattended CI). In a pipeline you lean hardest on the bottom two layers, because the top one has no human to ask.
Key insight
safety-strategy ≠ sandbox
They're different layers. Sandbox = where Codex can read/write/reach the network (an in-process boundary). safety-strategy = what OS privileges the whole Codex process has (sudo membership, which user it runs as). Set both: a tight sandbox AND drop-sudo.
The auto-fix-CI pattern: red build to pull request
The headline automation: when CI fails, fire a second workflow that lets Codex edit the code with workspace-write and then open a PR with the fix. The trigger is workflow_run filtered to a failed conclusion:
on:
workflow_run:
workflows: ["CI"]
types: [completed]
jobs:
autofix:
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
runs-on: ubuntu-latest
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
steps:
- uses: openai/codex-action@v1
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
prompt: "You are working in a Node.js monorepo with Jest tests. Fix the failing CI."
codex-args: '["--config","sandbox_mode=\"workspace-write\""]'
- uses: peter-evans/create-pull-request@v6
with:
branch: codex/auto-fix-${{ github.event.workflow_run.run_id }}
title: "Auto-fix failing CI via Codex"The moving parts:
- Trigger on failure only — the
if:guards onconclusion == 'failure'so it doesn't run on green builds. workspace-writeis required because, unlike the review job, this one must edit files. (Remember:codex execdefaults toread-only; you must opt in to writing.)- Open a PR, don't push to main. Routing the fix through a PR keeps a human review gate on AI-authored changes — exactly the oversight CI otherwise removes.
- Pin the action. The cookbook pins
@mainfor the demo; in production pin@v1or a SHA.
This pattern is powerful and risky in equal measure: an agent is now writing code on a trigger you don't watch. The mitigations from this whole lesson apply at once — scope the key, keep drop-sudo, write to a branch + PR (never main), and restrict who can cause the upstream workflow to run.
Example
Why a PR and not a direct push
Opening a PR (e.g. with peter-evans/create-pull-request) re-inserts the human that unattended CI removed: someone reviews the diff before merge. A direct git push to main would let an auto-triggered agent ship unreviewed code — the opposite of the safety posture you want.
Watch out
Loops and triggers
If the auto-fix PR itself triggers CI which can fail and re-trigger auto-fix, you can build an expensive loop. Filter triggers (branch names, paths) and watch usage. CI tasks count against the same usage limits as everything else.
Try it: Build an auto-fix-CI pipeline that opens a PR
Goal: stand up the full CI-auth + Action + auto-fix loop end-to-end on a throwaway repo. 1) Prep a repo with a failing test. Create (or fork) a small Node or Python repo with a CI workflow named CI that runs the test suite, then introduce one deliberately failing test so CI goes red. 2) Store the credential as a secret. Add OPENAI_API_KEY to the repo's Actions secrets — never commit it. (Have the user paste the key into the GitHub secrets UI; do not echo it anywhere.) 3) Add a review job first (read-only). Wire up openai/codex-action@v1 on pull_request with sandbox: read-only, a prompt-file, and permissions: contents: read. Confirm it posts a final-message and changes nothing. 4) Add the auto-fix job. New workflow triggered on workflow_run for CI, guarded by if: github.event.workflow_run.conclusion == 'failure'. Run the Action with workspace-write, then peter-evans/create-pull-request@v6 to open a PR on a codex/auto-fix-* branch. 5) Verify the safety posture. Check three things: the key is scoped (inline CODEX_API_KEY or a minimal secret, not a sprawling job env), safety-strategy is left at the drop-sudo default, and the fix lands in a PR, not a push to main. 6) Trip it. Push the failing test, watch CI fail, watch the auto-fix workflow open a PR with a candidate fix, and review the diff yourself before merging. 7) Write four sentences: which auth path you used and why, how you scoped the key, what safety-strategy bought you, and what could go wrong if the auto-fix PR re-triggered CI. This cements the lesson's core instinct — in CI, scope the credential and scope the power, because there's no human approval prompt to save you.
Key takeaways
- 1Two CI auth paths: scoped API key (recommended, simpler) or a seeded ChatGPT `auth.json`. Prefer `CODEX_API_KEY=... codex exec ...` inline so only one process can read the key, not a job-wide `OPENAI_API_KEY` export.
- 2For ChatGPT auth in CI, seed `~/.codex/auth.json` from a secret with `cli_auth_credentials_store="file"`; Codex refreshes the token (~8 days/on 401) and writes it back — so seed-once-per-persistent-runner, one file per runner, and never overwrite the refreshed file.
- 3Headless login without a browser: `codex login --device-auth` (one-time code) or SSH-forward the localhost OAuth callback on port 1455. Usually you generate `auth.json` once and store it as a secret.
- 4The official `openai/codex-action@v1` runs `codex exec` in CI; key inputs are `openai-api-key`, `prompt`/`prompt-file`, `sandbox`, `model`, `effort`, and `output-schema`, with `final-message` as the output. Pin to `@v1` or a SHA, never `@main`.
- 5`safety-strategy` reduces OS privilege independently of the sandbox: `drop-sudo` (default), `unprivileged-user`, `read-only`, and `unsafe` (required on Windows). Keep it tight to stop a compromised run from exfiltrating the key.
- 6Auto-fix-CI: trigger on `workflow_run` with `conclusion == 'failure'`, run Codex with `workspace-write`, and open a PR (never push to main) so a human still reviews AI-authored fixes.
Quiz
Lock in what you learned
Check your understanding
0 / 4 answered
1.Why does the documentation recommend setting `CODEX_API_KEY` inline on the `codex exec` command rather than exporting `OPENAI_API_KEY` in a job-wide `env:` block?
2.You run Codex with ChatGPT auth on a long-lived self-hosted runner by re-writing `auth.json` from a secret at the start of every job. After about a week, auth starts failing. What is the most likely cause?
3.In the official `openai/codex-action@v1`, what does the `safety-strategy: drop-sudo` setting do, and why is it the recommended default?
4.In the auto-fix-CI pattern, why route Codex's fix through a pull request (e.g. `peter-evans/create-pull-request`) rather than having it `git push` directly to `main`?
Go deeper
Hand-picked sources to keep learning
Seeding auth.json from a secret, file storage, and the token-refresh behavior on runners.
OPENAI_API_KEY vs CODEX_API_KEY, login subcommands, and --device-auth headless login.
Headless execution, the read-only default sandbox, and the exec-only CODEX_API_KEY recommendation.
Action inputs, safety strategies, the final-message output, and an example PR-review workflow.
Source of truth for input names, safety-strategy options, and the stable @v1 tag to pin.
Worked auto-fix-CI workflow: trigger on failure, edit with workspace-write, open a PR.