auth/ci-cd-auth.md +277 −0 added
1# Maintain Codex account auth in CI/CD (advanced)
2
3This guide shows how to keep ChatGPT-managed Codex auth working on a trusted
4CI/CD runner without calling the OAuth token endpoint yourself.
5
6The right way to authenticate automation is with an API key. Use this guide
7only if you specifically need to run the workflow as your Codex account.
8
9The pattern is:
10
111. Create `auth.json` once on a trusted machine with `codex login`.
122. Put that file on the runner.
133. Run Codex normally.
144. Let Codex refresh the session when it becomes stale.
155. Keep the refreshed `auth.json` for the next run.
16
17This is an advanced workflow for enterprise and other trusted private
18automation. API keys are still the recommended option for most CI/CD jobs.
19
20Treat `~/.codex/auth.json` like a password: it contains access tokens. Don't
21 commit it, paste it into tickets, or share it in chat. Do not use this
22 workflow for public or open-source repositories.
23
24## Why this works
25
26Codex already knows how to refresh a ChatGPT-managed session.
27
28As of the current open-source client:
29
30- Codex loads the local auth cache from `auth.json`
31- if `last_refresh` is older than about 8 days, Codex refreshes the token
32 bundle before the run continues
33- after a successful refresh, Codex writes the new tokens and a new
34 `last_refresh` back to `auth.json`
35- if a request gets a `401`, Codex also has a built-in refresh-and-retry path
36
37That means the supported CI/CD strategy is not "call the refresh API yourself."
38It is "run Codex and persist the updated `auth.json`."
39
40## When to use this
41
42Use this guide only when all of the following are true:
43
44- you need ChatGPT-managed Codex auth rather than an API key
45- `codex login` cannot run on the remote runner
46- the runner is trusted private infrastructure
47- you can preserve the refreshed `auth.json` between runs
48- only one machine or serialized job stream will use a given `auth.json` copy
49
50This guide applies to Codex-managed ChatGPT auth (`auth_mode: "chatgpt"`).
51
52It does not apply to:
53
54- API key auth
55- external-token host integrations (`auth_mode: "chatgptAuthTokens"`)
56- generic OAuth clients outside Codex
57
58If your credentials are stored in the OS keyring, switch to file-backed storage
59first. See [Credential storage](https://developers.openai.com/codex/auth#credential-storage).
60
61## Seed `auth.json` once
62
63On a trusted machine where browser login is possible:
64
651. Configure Codex to store credentials in a file:
66
67```toml
68cli_auth_credentials_store = "file"
69```
70
712. Run:
72
73```bash
74codex login
75```
76
773. Verify the file looks like managed ChatGPT auth:
78
79```bash
80AUTH_FILE="${CODEX_HOME:-$HOME/.codex}/auth.json"
81
82jq '{
83 auth_mode,
84 has_tokens: (.tokens != null),
85 has_refresh_token: ((.tokens.refresh_token // "") != ""),
86 last_refresh
87}' "$AUTH_FILE"
88```
89
90Continue only if:
91
92- `auth_mode` is `"chatgpt"`
93- `has_refresh_token` is `true`
94
95Then place the contents of `auth.json` into your CI/CD secret manager or copy
96it to a trusted persistent runner.
97
98## Recommended pattern: GitHub Actions on a self-hosted runner
99
100The simplest fully automated setup is a self-hosted GitHub Actions runner with a
101persistent `CODEX_HOME`.
102
103Why this pattern works well:
104
105- the runner can keep `auth.json` on disk between jobs
106- Codex can refresh the file in place
107- later jobs automatically pick up the refreshed tokens
108- you only need the original secret for bootstrap or reseeding
109
110The critical detail is to seed `auth.json` only if it is missing. If you
111rewrite the file from the original secret on every run, you throw away the
112refreshed tokens that Codex just wrote.
113
114Example scheduled workflow:
115
116```yaml
117name: Keep Codex auth fresh
118
119on:
120 schedule:
121 - cron: "0 9 * * 1"
122 workflow_dispatch:
123
124jobs:
125 keep-codex-auth-fresh:
126 runs-on: self-hosted
127 steps:
128 - name: Bootstrap auth.json if needed
129 shell: bash
130 env:
131 CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }}
132 run: |
133 export CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
134 mkdir -p "$CODEX_HOME"
135 chmod 700 "$CODEX_HOME"
136
137 if [ ! -f "$CODEX_HOME/auth.json" ]; then
138 printf '%s' "$CODEX_AUTH_JSON" > "$CODEX_HOME/auth.json"
139 chmod 600 "$CODEX_HOME/auth.json"
140 fi
141
142 - name: Run Codex
143 shell: bash
144 run: |
145 codex exec --json "Reply with the single word OK." >/dev/null
146```
147
148What this does:
149
150- the first run seeds `auth.json`
151- later runs reuse the same file
152- once the cached session is old enough, Codex refreshes it during the normal
153 `codex exec` step
154- the refreshed file remains on disk for the next workflow run
155
156A weekly schedule is usually enough because Codex treats the session as stale
157after roughly 8 days in the current open-source client.
158
159## Ephemeral runners: restore, run Codex, persist the updated file
160
161If you use GitHub-hosted runners, GitLab shared runners, or any other ephemeral
162environment, the runner filesystem disappears after each job. In that setup,
163you need a round-trip:
164
1651. restore the current `auth.json` from secure storage
1662. run Codex
1673. write the updated `auth.json` back to secure storage
168
169Generic GitHub Actions shape:
170
171```yaml
172name: Run Codex with managed auth
173
174on:
175 workflow_dispatch:
176
177jobs:
178 codex-job:
179 runs-on: ubuntu-latest
180 steps:
181 - name: Restore auth.json
182 shell: bash
183 run: |
184 export CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
185 mkdir -p "$CODEX_HOME"
186 chmod 700 "$CODEX_HOME"
187
188 # Replace this with your secret manager or secure storage command.
189 my-secret-cli read codex-auth-json > "$CODEX_HOME/auth.json"
190 chmod 600 "$CODEX_HOME/auth.json"
191
192 - name: Run Codex
193 shell: bash
194 run: |
195 codex exec --json "summarize the failing tests"
196
197 - name: Persist refreshed auth.json
198 if: always()
199 shell: bash
200 run: |
201 # Replace this with your secret manager or secure storage command.
202 my-secret-cli write codex-auth-json < "$CODEX_HOME/auth.json"
203```
204
205The key requirement is that the write-back step stores the refreshed file that
206Codex produced during the run, not the original seed.
207
208## You do not need a separate refresh command
209
210Any normal Codex run can refresh the session.
211
212That means you have two good options:
213
214- let your existing CI/CD Codex job refresh the file naturally
215- add a lightweight scheduled maintenance job, like the GitHub Actions example
216 above, if your real jobs do not run often enough
217
218The first Codex run after the session becomes stale is the one that refreshes
219`auth.json`.
220
221## Operational rules that matter
222
223- Use one `auth.json` per runner or per serialized workflow stream.
224- Do not share the same file across concurrent jobs or multiple machines.
225- Do not overwrite a persistent runner's refreshed file from the original seed
226 on every run.
227- Do not store `auth.json` in the repository, logs, or public artifact storage.
228- Reseed from a trusted machine if built-in refresh stops working.
229
230## What to do when refresh stops working
231
232This flow reduces manual work, but it does not guarantee the same session lasts
233forever.
234
235Reseed the runner with a fresh `auth.json` if:
236
237- Codex starts returning `401` and the runner can no longer refresh
238- the refresh token was revoked or expired
239- another machine or concurrent job rotated the token first
240- your secure-storage round trip failed and an old file was restored
241
242To reseed:
243
2441. Run `codex login` on a trusted machine.
2452. Replace the stored CI/CD copy of `auth.json`.
2463. Let the next runner job continue using Codex's built-in refresh flow.
247
248## Verify that the runner is maintaining the session
249
250Check that the runner still has managed auth tokens and that `last_refresh`
251exists:
252
253```bash
254AUTH_FILE="${CODEX_HOME:-$HOME/.codex}/auth.json"
255
256jq '{
257 auth_mode,
258 last_refresh,
259 has_access_token: ((.tokens.access_token // "") != ""),
260 has_id_token: ((.tokens.id_token // "") != ""),
261 has_refresh_token: ((.tokens.refresh_token // "") != "")
262}' "$AUTH_FILE"
263```
264
265If your runner is persistent, you should see the same file continue to exist
266between runs. If your runner is ephemeral, confirm that your write-back step is
267storing the updated file from the last job.
268
269## Source references
270
271If you want to verify this behavior in the open-source client:
272
273- [`codex-rs/core/src/auth.rs`](https://github.com/openai/codex/blob/main/codex-rs/core/src/auth.rs)
274 covers stale-token detection, automatic refresh, refresh-on-401 recovery, and
275 persistence of refreshed tokens
276- [`codex-rs/core/src/auth/storage.rs`](https://github.com/openai/codex/blob/main/codex-rs/core/src/auth/storage.rs)
277 covers file-backed `auth.json` storage