1# Run Codex Security in GitLab CI/CD
2
3> For the complete documentation index, see [llms.txt](https://learn.chatgpt.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.
4
5Run Codex Security in GitLab CI/CD to scan committed changes and protected
6branches, publish findings to GitLab Security, and optionally propose verified
7fixes in draft merge requests.
8
9The workflow keeps scan credentials separate from repository-write access.
10Generated changes always require human review before merge.
11
12Start with scan-only reporting. Enable remediation only after you check the
13runner, findings, and credential boundaries for your project.
14
15## Before you begin
16
17You need:
18
19- A GitLab project with a trusted runner that supports the Codex sandbox's
20 user namespace.
21- The Maintainer or Owner role in the GitLab project so you can configure
22 [project CI/CD variables](https://docs.gitlab.com/ci/variables/) and protected
23 resources.
24- An OpenAI API key with Codex Security access. Organizations using Platform
25 API keys can [request Trusted Access for
26 Cyber](https://openai.com/form/enterprise-trusted-access-for-cyber/).
27 Individuals using ChatGPT authentication can use the [personal Trusted Access
28 flow](https://chatgpt.com/cyber). Some accounts or repositories require this
29 access for full-repository scans.
30- GitLab Ultimate 19.2 or later for [SARIF 2.1.0
31 ingestion](https://docs.gitlab.com/user/application_security/detect/sarif/).
32- Full Git history so merge request jobs can calculate the merge base.
33
34The pipeline image installs Node.js 26, Python 3, Git, `rg`, and the pinned
35Codex Security CLI. Automated remediation additionally requires an existing
36regression test and a runner that can run repository-controlled commands
37without protected credentials.
38
39## Start with a scan-only pipeline
40
41Create a masked, hidden, protected GitLab CI/CD variable named
42`CODEX_SECURITY_API_KEY`. Use an OpenAI Platform API key with Codex Security
43access, and set its environment scope to `codex-security/openai`. See
44[environment-scoped CI/CD variables](https://docs.gitlab.com/ci/environments/#limit-the-environment-scope-of-a-cicd-variable).
45
46Add this minimal pipeline to a test project first. It scans committed changes
47in eligible protected merge requests, publishes SARIF from a successful report
48job, and restores the scanner result in a separate gate:
49
50```yaml
51stages:
52 - security_scan
53 - security_gate
54
55.codex-security-merge-request:
56 rules:
57 - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_SOURCE_PROJECT_ID == $CI_PROJECT_ID && $CI_MERGE_REQUEST_SOURCE_BRANCH_PROTECTED == "true" && $CI_MERGE_REQUEST_TARGET_BRANCH_PROTECTED == "true"'
58
59codex-security:
60 extends: .codex-security-merge-request
61 stage: security_scan
62 image: node:26-bookworm-slim
63 environment:
64 name: codex-security/openai
65 action: access
66 variables:
67 GIT_DEPTH: "0"
68 before_script:
69 - npm install --prefix /tmp/codex-security-cli --ignore-scripts --no-audit --no-fund @openai/codex-security@0.1.20
70 script:
71 - |
72 set -eu
73 test -n "${CODEX_SECURITY_API_KEY:-}"
74
75 CODEX_SECURITY_BIN="/tmp/codex-security-cli/node_modules/.bin/codex-security"
76 RESULTS_DIR="/tmp/codex-security-results-$CI_JOB_ID"
77 ARTIFACT_DIR="codex-security-artifacts"
78 BASE_REVISION="$(git merge-base \
79 "$CI_MERGE_REQUEST_DIFF_BASE_SHA" "$CI_COMMIT_SHA")"
80 install -d -m 700 "$RESULTS_DIR" "$ARTIFACT_DIR/results"
81
82 codex_security_api_key="$CODEX_SECURITY_API_KEY"
83 unset CODEX_SECURITY_API_KEY
84 set +e
85 OPENAI_API_KEY="$codex_security_api_key" \
86 "$CODEX_SECURITY_BIN" scan . \
87 --diff "$BASE_REVISION" \
88 --head "$CI_COMMIT_SHA" \
89 --auth api-key \
90 --output-dir "$RESULTS_DIR" \
91 --json
92 scan_exit="$?"
93 set -e
94 unset codex_security_api_key
95
96 case "$scan_exit" in
97 0|1|2) ;;
98 *) exit "$scan_exit" ;;
99 esac
100
101 "$CODEX_SECURITY_BIN" export "$RESULTS_DIR" \
102 --export-format sarif \
103 --source-root "$CI_PROJECT_DIR" \
104 --output "$ARTIFACT_DIR/results.sarif"
105 test -s "$ARTIFACT_DIR/results.sarif"
106 cp -R "$RESULTS_DIR"/. "$ARTIFACT_DIR/results/"
107 printf '%s\n' "$scan_exit" > "$ARTIFACT_DIR/scan-exit-code.txt"
108 exit 0
109 artifacts:
110 when: always
111 access: maintainer
112 expire_in: 7 days
113 paths:
114 - codex-security-artifacts/
115 reports:
116 sarif: codex-security-artifacts/results.sarif
117
118codex-security-gate:
119 extends: .codex-security-merge-request
120 stage: security_gate
121 image: alpine:3.20
122 needs:
123 - job: codex-security
124 artifacts: true
125 script:
126 - exit "$(cat codex-security-artifacts/scan-exit-code.txt)"
127```
128
129
130
131> Illustration: GitLab pipeline scans a committed diff, publishes a SARIF report, and restores the scan result in a policy gate
132
133
134
135Review every change to `.gitlab-ci.yml` before running a secret-bearing job.
136The minimal example intentionally omits full scans and remediation.
137
138## Adopt the production pipeline
139
1401. [Download the complete GitLab pipeline](https://learn.chatgpt.com/docs/security/cli/ci/gitlab.yml)
141 and save it as `.gitlab-ci.yml` in the repository root. If your repository
142 already has a pipeline, merge the example's stages, hidden templates, and
143 jobs into the existing file.
1442. Preserve existing build, test, and deployment stages. If the project uses
145 `workflow: rules`, confirm that it allows the pipeline events you want to
146 scan.
147
148The example adds `security_scan`, `security_remediation`, `security_publish`,
149and `security_gate` stages. Scan-only reporting requires only
150`CODEX_SECURITY_API_KEY`.
151
152The scan job runs by default only for same-project merge requests between
153protected branches. Set `CODEX_SECURITY_FULL_SCAN_DEFAULT_BRANCH=true` to scan
154protected default-branch pushes and manual pipelines. Set
155`CODEX_SECURITY_SCHEDULED_DEEP_SCAN=true` and configure explicit time and cost
156budgets to enable scheduled deep scans on the protected default branch.
157
158A merge request pipeline can access protected variables and runners only when:
159
160- You protect the source and target branches in the same project.
161- The project [permits merge request pipelines to access protected variables and
162 runners](https://docs.gitlab.com/ci/pipelines/merge_request_pipelines/#control-access-to-protected-variables-and-runners).
163- The user who starts the pipeline can push or merge into the target branch.
164
165Fork pipelines and unprotected merge requests don't receive the scan
166credential. Review every change to `.gitlab-ci.yml` before running a
167secret-bearing job. Masking and hiding a variable do not make untrusted CI code
168safe.
169
170## Run a scan and review findings
171
172Create an eligible protected merge request or run the pipeline on the
173protected default branch. Start with a small diff before running a paid
174full-repository scan.
175
176Open the `codex-security` job and confirm that its artifacts include:
177
178- `scan-manifest.json`
179- `findings.json`
180- `coverage.json`
181- `results.sarif`
182- `scan-exit-code.txt`
183
184Then open the pipeline **Security** tab, review ingestion warnings, and confirm
185finding identifiers, severity levels, and source locations. Default-branch scans
186also create project vulnerability records. Merge request findings appear in
187the pipeline Security tab or merge request security widget but don't create
188project-wide vulnerability records.
189
190Restrict artifact access because scan results can contain vulnerable source
191snippets, evidence, and remediation details.
192
193## Choose a scan profile
194
195The pipeline selects a profile from the trigger:
196
197| Trigger | Target | Mode | Effort |
198| ---------------------------------------------- | --------------- | ---------- | ------- |
199| Protected same-project merge request | Committed diff | `standard` | `low` |
200| Opt-in protected default-branch push or manual | Full repository | `standard` | `high` |
201| Opt-in schedule on protected default branch | Full repository | `deep` | `xhigh` |
202
203Merge request scans focus feedback on the committed change.
204Default-branch scans review the integrated repository. Scheduled deep scans
205provide broader periodic coverage. A completed diff scan applies only to that
206change and does not show that the entire repository is clean.
207
208The workflow installs the CLI outside the repository and runs it by absolute
209path. Its dry-run preflight uses the process-scoped API key but does not start a
210paid scan or verify API authentication, Codex Security access, quota, or model
211availability.
212
213The workflow writes scan state and results outside the worktree and scopes
214`OPENAI_API_KEY` to the scan process. The CLI receives a small, explicit
215environment instead of inheriting every GitLab variable. For diff scans, the
216workflow calculates the merge base and binds the scan to the reviewed base and
217head revisions.
218
219The example pins `@openai/codex-security` to `0.1.20`. Retest authentication,
220artifacts, SARIF ingestion, and policy gating before changing the pin.
221
222## Separate reporting from policy enforcement
223
224GitLab ingests SARIF from a successful report job. The pipeline publishes the
225report first and restores the scanner's exit status in a separate
226`codex-security-gate` job.
227
228The report job accepts findings from exit codes `0` and `1`. It accepts exit
229code `2` only when the scan manifest proves the scan completed, coverage is
230explicitly `partial`, and a non-empty SARIF report exists. Other runtime,
231configuration, or export failures remain blocking.
232
233The final gate preserves these scanner exit codes:
234
235| Exit | Meaning |
236| ---- | --------------------------------------------------------------------------- |
237| `0` | The scan completed with complete coverage and passed its policy. |
238| `1` | The scan completed and found an issue at or above the configured threshold. |
239| `2` | The scan had incomplete coverage or an input or runtime error. |
240
241The example temporarily allows exit `2` while you calibrate partial coverage.
242Remove that allowance when incomplete coverage must block the pipeline.
243
244Remediation and publishing run before the final policy gate. An eligible
245finding can produce a verified draft merge request even when the gate later
246fails the pipeline.
247
248## Enable verified remediation
249
250Automated remediation is optional and runs only for protected default-branch
251pipelines. The Codex remediation process and repository-controlled verification
252commands do not receive the GitLab project access token or runner-injected
253credentials.
254
255The security contract has three parts: repository-controlled commands never
256receive OpenAI or GitLab credentials, only the publishing job receives
257repository-write access, and every generated change stays a draft until a
258human reviews and merges it.
259
260The workflow:
261
2621. Requires complete scan coverage and a `high`- or `critical`-severity
263 finding.
2642. Confirms that the configured regression test fails before patching.
2653. Generates a focused patch and rejects changes to CI, credential, binary, or
266 other protected files.
2674. Runs the regression test without OpenAI, GitLab, registry, deployment, or
268 job-token credentials.
2695. Uses `verify-fix` to return `fixed`, `still_vulnerable`, or `inconclusive`.
270 The job publishes a patch only when `verify-fix` returns `fixed` and the
271 verification process leaves the patch unchanged.
272
273Set these protected variables to enable remediation:
274
275- Set `CODEX_SECURITY_ENABLE_REMEDIATION` to `true`.
276- Set `CODEX_SECURITY_VERIFICATION_COMMAND` to an existing regression test that
277 exits `1` before the fix and `0` afterward.
278- Optionally, set `CODEX_SECURITY_SETUP_COMMAND` to a non-interactive dependency
279 setup command.
280
281Choose a regression test that exercises the underlying security invariant, not
282a particular implementation. Apply the same scrutiny to generated test and
283source changes.
284
285<details>
286 <summary>Advanced: repository command isolation</summary>
287
288The `validate`, `patch`, and `verify-fix` commands receive a process-scoped
289`CODEX_API_KEY`. Repository-controlled setup and test commands run as a
290separate unprivileged user in a writable copy of the tracked source files.
291The copy intentionally excludes Git metadata, submodule contents, and
292downloaded artifacts. Setup and test commands that require `.git` or
293submodules must run in a separately designed credential-free job.
294
295Only the root-owned Codex steps can access the canonical checkout or GitLab's
296adjacent file-variable directory. The copy's clean environment contains only
297`PATH`, `HOME`, `LANG`, `CI`, and `CI_PROJECT_DIR`. If a command needs another
298non-secret value, add it to the allowlist after reviewing the command. If your
299runner cannot change users, move verification into a separate credential-free
300job before enabling remediation.
301
302</details>
303
304## Publish a draft merge request
305
306Create a [GitLab project access
307token](https://docs.gitlab.com/user/project/settings/project_access_tokens/#create-a-project-access-token)
308with the Developer role and the `api` and `write_repository` scopes. Store it as
309a protected, masked, hidden `GITLAB_REMEDIATION_TOKEN` scoped only to the
310`codex-security/publish` environment.
311
312Set `CODEX_SECURITY_CREATE_MR=true` to enable publishing. Also set the non-secret
313`CODEX_SECURITY_MR_TEST_COMMAND` to the project-specific security regression
314test that every generated remediation branch must pass. Keep this variable
315unprotected so the generated unprotected merge request can read the command.
316The publishing workflow:
317
318- Receives the repository-write token but no OpenAI credential.
319- Creates a `codex-security/fix-<finding-hash>` branch.
320- Opens a draft merge request and reuses an existing open draft instead of
321 creating a duplicate.
322- Runs the unprotected remediation branch's regression test as an unprivileged
323 user in a tracked-only copy without protected credentials.
324- Never merges the generated change automatically.
325
326Don't substitute `CI_JOB_TOKEN` for the project access token. It cannot perform
327the required merge request creation operation. Review the proposed patch,
328verification evidence, and finding before merging.
329
330## Configure optional variables
331
332Configure only the variables needed for the features you enable:
333
334| Variable | When needed | Default or purpose |
335| ----------------------------------------- | --------------------------------- | ----------------------------------------------------------- |
336| `CODEX_SECURITY_API_KEY` | Every scan | Protected, masked, hidden; scope to `codex-security/openai` |
337| `CODEX_SECURITY_VERSION` | CLI upgrade | Pinned to `0.1.20`; retest before changing |
338| `CODEX_SECURITY_FULL_SCAN_DEFAULT_BRANCH` | Default-branch full scans | Explicit opt-in; off by default |
339| `CODEX_SECURITY_SCHEDULED_DEEP_SCAN` | Scheduled deep scans | Explicit opt-in; off by default |
340| `CODEX_SECURITY_DEEP_MAX_TIME_HOURS` | Scheduled deep scans | Required time budget greater than `0` and less than `8` |
341| `CODEX_SECURITY_DEEP_MAX_COST` | Scheduled deep scans | Required estimated USD cost guardrail greater than `0` |
342| `CODEX_SECURITY_ENABLE_REMEDIATION` | Patch generation | Protected opt-in; off by default |
343| `CODEX_SECURITY_VERIFICATION_COMMAND` | Patch generation | Protected regression test |
344| `CODEX_SECURITY_SETUP_COMMAND` | Optional remediation setup | Protected dependency installation |
345| `CODEX_SECURITY_REMEDIATION_EFFORT` | Optional remediation tuning | `high` |
346| `CODEX_SECURITY_MAX_CHANGED_FILES` | Optional patch-size limit | `8`; allowed range `1` through `20` |
347| `CODEX_SECURITY_CREATE_MR` | Draft merge request creation | Protected opt-in; off by default |
348| `GITLAB_REMEDIATION_TOKEN` | Draft merge request creation | Developer project token scoped to `codex-security/publish` |
349| `CODEX_SECURITY_GITLAB_INTERNAL_URL` | Optional self-hosted publishing | GitLab origin reachable from the runner |
350| `CODEX_SECURITY_MR_TEST_COMMAND` | Draft merge request publishing | Required non-secret, project-specific regression test |
351| `CODEX_SECURITY_MR_SETUP_COMMAND` | Optional remediation branch setup | Non-secret dependency setup |
352
353GitLab supplies the `CI_*` variables. The pipeline manages
354`CODEX_SECURITY_BIN`, `CODEX_SECURITY_EFFORT`, `CODEX_SECURITY_MODE`,
355`CODEX_SECURITY_STATE_DIR`, and `CODEX_SECURITY_TARGET`; don't configure them
356as project variables. For diff scans, the CLI derives the canonical target
357identity from the normalized base and head revisions.
358
359## Tune enforcement and cost
360
361Use focused diff scans for merge request feedback, standard repository scans
362for the default branch, and scheduled deep scans for broader coverage. Both
363full-repository profiles are off by default. A scheduled deep scan also requires
364`CODEX_SECURITY_DEEP_MAX_TIME_HOURS` and `CODEX_SECURITY_DEEP_MAX_COST`; keep the
365CLI time budget below the job's eight-hour timeout. Measure representative runs
366before setting a budget. Treat `--max-cost` as an estimated cost guardrail, not
367a hard billing cap.
368
369Start with report-only scans. Add `--fail-on-severity` after your team has
370reviewed representative findings, coverage, cost, and runtime. See [Run Codex
371Security in CI](https://learn.chatgpt.com/docs/security/cli/ci) for severity policies and exit-code
372details.
373
374When a job fails:
375
376- Missing scan artifacts point to a configuration or runner problem.
377- Existing artifacts with partial coverage require reviewing `coverage.json`.
378- Missing GitLab findings require checking whether the SARIF report job
379 succeeded and whether GitLab accepted the report.
380- Skipped remediation requires checking the protected branch, complete
381 coverage, finding severity, verification command, and opt-in variables.
382- Publishing errors require checking the project token's role, scopes, and
383 environment restriction.
384
385For every command, flag, and artifact, see the [Codex Security CLI
386reference](https://learn.chatgpt.com/docs/security/cli/reference).