Skip to content

Run Codex in CI/CD with the GitHub Action

The Codex GitHub Action, openai/codex-action@v1, installs the Codex CLI on a GitHub Actions runner and runs codex exec with a prompt from your repository. It fits four CI jobs: PR review comments, a structured findings gate, draft release notes, and CI auto-fix. Its safety comes from safety-strategy, a permission profile, and splitting privileged steps into separate jobs.

This page is for the developer or tech lead who owns the pipeline. Your CI already runs lint, type checks, and tests. It does not catch an unauthenticated route, a query built from string concatenation, or a 400-line function nobody asked for, and your reviewers keep finding those by hand. You want Codex to look at every PR before a human does, without handing an agent your repository’s write token.

  • Four workflows you can copy: a PR review comment, a findings gate that fails the build on P0 issues, draft release notes, and a CI auto-fix that opens a draft PR.
  • Prompt and schema files under .github/codex/, so review criteria are versioned and reviewed like code.
  • A hardened setup: drop-sudo, a permission profile, persist-credentials: false, untrusted values passed through env:, and the agent’s output handed to a separate job before anything is written.
  • A way to decide when Codex findings can block a merge, based on precision you measure rather than trust.

Pick by what the job is allowed to change. The more it can write, the more of the pipeline you split into separate jobs.

JobTriggerPermission profileWhat Codex can changeWho signs off
PR review commentpull_request:read-onlyNothing; a second job posts the commentThe PR’s human reviewer
Findings gatepull_request:read-onlyNothing; a second job fails on P0Code owner of .github/codex/
Release notestag push:read-onlyNothing; a second job creates a draft releaseRelease manager publishes it
CI auto-fixworkflow_run:workspaceThe checkout only; a second job opens a draft PRReviewer of the draft PR

If you want review comments and do not need control over the prompt, OpenAI’s action README points to the managed code review in Codex cloud instead; see code review with Codex. For CI systems other than GitHub Actions, call codex exec directly as described in headless and non-interactive mode.

  1. Create an OpenAI API key for CI only, and store it as the repository secret OPENAI_API_KEY (Settings > Secrets and variables > Actions). A dedicated key lets you rotate or cap it without touching anyone’s laptop.

  2. Create .github/codex/prompts/ and .github/codex/schemas/, then add them to CODEOWNERS. On pull_request runs, the workflow, prompt, and schema all come from the PR’s merge commit, so a PR can weaken its own review unless an owner must approve changes to them. Then enable Require review from Code Owners in the branch protection rule or ruleset for your default branch; without it, CODEOWNERS only requests a review:

    # .github/CODEOWNERS
    /.github/ @your-org/platform
  3. Leave model and effort empty at first. The action then uses the Codex CLI’s default: GPT-6 Astra at low effort as of CLI 0.157.1 (see the models hub). If reviews are shallow, raise effort before you change model.

  4. Do not pin codex-version below 0.138.0. Permission profiles, which every workflow on this page uses, need that version or later.

This workflow follows the shape of the action’s own README example, with these changes: the prompt lives in a file, it does not paste the PR title and body into the prompt, and Codex runs with the :read-only profile instead of :workspace because a review never needs to write. It also replaces the README’s pre-fetch step with fetch-depth: 2, adds synchronize, reopened and ready_for_review to the README’s opened trigger, and adds a concurrency group, a draft skip and timeout-minutes (see the failure modes below). The README’s example still uses actions/checkout@v5 and actions/github-script@v7; this page uses the current majors, actions/checkout@v7, actions/github-script@v9 and the matching artifact actions (checked 2026-10-03). Replace them with the versions your organization pins before you copy a workflow.

.github/workflows/codex-review.yml
name: Codex PR review
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
concurrency:
group: codex-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
codex:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
outputs:
final_message: ${{ steps.run_codex.outputs.final-message }}
steps:
- uses: actions/checkout@v7
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
fetch-depth: 2
persist-credentials: false
- 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
permission-profile: ":read-only"
post_feedback:
runs-on: ubuntu-latest
needs: codex
if: needs.codex.outputs.final_message != ''
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/github-script@v9
env:
CODEX_FINAL_MESSAGE: ${{ needs.codex.outputs.final_message }}
with:
github-token: ${{ github.token }}
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: process.env.CODEX_FINAL_MESSAGE,
});

Three details carry the security. The codex job holds only contents: read, and the job with write access never runs Codex. persist-credentials: false keeps the token out of .git/config, where Codex could read it. No untrusted value reaches a script: the PR title and body stay out of the prompt, and Codex’s comment reaches github-script through env: (CODEX_FINAL_MESSAGE), because GitHub expands ${{ }} before the script runs and would splice the text into the code.

Fail the build on P0 findings with structured output

Section titled “Fail the build on P0 findings with structured output”

A comment is advisory. To let findings block a merge, make Codex return JSON that a script can check. The action’s output-schema-file input passes a JSON Schema file to codex exec --output-schema, so the final message has a fixed shape. (The --output-schema flag takes a file path, not inline JSON; use the separate output-schema input if you want the schema inline in the workflow.)

Save the schema as .github/codex/schemas/findings.json:

{
"type": "object",
"additionalProperties": false,
"required": ["findings"],
"properties": {
"findings": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["severity", "file", "line", "title", "fix"],
"properties": {
"severity": { "type": "string", "enum": ["P0", "P1", "P2"] },
"file": { "type": "string" },
"line": { "type": "integer" },
"title": { "type": "string" },
"fix": { "type": "string" }
}
}
}
}
}

Then run the scan on the paths where a miss is expensive, and check the result in a second job whose token has no permissions:

.github/workflows/codex-security.yml
name: Codex security gate
on:
pull_request:
paths:
- 'src/routes/**'
- 'src/middleware/**'
- 'src/lib/db/**'
- 'package.json'
jobs:
scan:
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
outputs:
findings: ${{ steps.codex.outputs.final-message }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 2
- id: codex
uses: openai/codex-action@v1
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
prompt-file: .github/codex/prompts/security.md
output-schema-file: .github/codex/schemas/findings.json
permission-profile: ":read-only"
gate:
runs-on: ubuntu-latest
needs: scan
permissions: {}
steps:
- name: Summarize and fail on P0
env:
FINDINGS: ${{ needs.scan.outputs.findings }}
run: |
echo "$FINDINGS" | jq -r '.findings[] | "- **\(.severity)** `\(.file):\(.line)` \(.title)"' >> "$GITHUB_STEP_SUMMARY"
echo "$FINDINGS" | jq -e '[.findings[] | select(.severity == "P0")] | length == 0'

jq -e exits non-zero when the expression is false, so the gate job fails exactly when a P0 finding exists. If Codex returns something that is not valid JSON, jq fails too, which is the safe direction.

Release notes are a good first write-adjacent job, because the output is a draft a person publishes. Codex reads history with :read-only; a second job with contents: write creates the draft release from Codex’s final message.

.github/workflows/codex-release-notes.yml
name: Codex release notes
on:
push:
tags: ['v*']
jobs:
notes:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
notes: ${{ steps.codex.outputs.final-message }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- id: codex
uses: openai/codex-action@v1
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
permission-profile: ":read-only"
prompt: |
Write release notes for tag ${{ github.ref_name }}.
Find the previous tag with `git describe --tags --abbrev=0 HEAD^`
and read `git log --no-merges PREVIOUS..HEAD`, opening diffs where
a commit message is unclear.
Output Markdown only, in Keep a Changelog groups: Added, Changed,
Fixed, Security. One user-facing line per change, no commit hashes.
End with one line: "Suggested bump: major|minor|patch" and why.
draft-release:
runs-on: ubuntu-latest
needs: notes
if: needs.notes.outputs.notes != ''
permissions:
contents: write
steps:
- env:
GH_TOKEN: ${{ github.token }}
NOTES: ${{ needs.notes.outputs.notes }}
run: gh release create "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --draft --title "$GITHUB_REF_NAME" --notes "$NOTES"

The auto-fix job is the only one where Codex edits files, so it is split three ways: a trigger that ignores forks and its own branches, a Codex job that can write only the checkout, and a job that turns the patch into a draft PR without running any repository code.

.github/workflows/codex-autofix.yml
name: Codex CI autofix
on:
workflow_run:
workflows: ["CI"]
types: [completed]
jobs:
propose-fix:
if: >-
github.event.workflow_run.conclusion == 'failure' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_repository.full_name == github.repository &&
!startsWith(github.event.workflow_run.head_branch, 'codex/fix-')
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
actions: read
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.workflow_run.head_sha }}
persist-credentials: false
- name: Save the failing log
env:
GH_TOKEN: ${{ github.token }}
RUN_ID: ${{ github.event.workflow_run.id }}
run: |
mkdir -p .codex-ci && echo ".codex-ci/" >> .git/info/exclude
gh run view "$RUN_ID" --repo "$GITHUB_REPOSITORY" --log-failed > .codex-ci/failure.log
- run: npm ci # the :workspace profile has no network, so install first
- uses: openai/codex-action@v1
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
permission-profile: ":workspace"
prompt: |
CI failed. The failing job log is in .codex-ci/failure.log.
Find the root cause and make the smallest change that fixes it.
Do not edit tests to make them pass, do not touch .github/, and do
not refactor unrelated code. Run the failing test command to prove
the fix and leave the change uncommitted in the working tree.
If you cannot fix it with confidence, change nothing and explain
why in your final message.
- name: Build the patch outside Codex
run: |
git add -A
git diff --cached --quiet || git diff --cached --binary --no-renames > "$RUNNER_TEMP/fix.patch"
- uses: actions/upload-artifact@v7
with:
name: codex-fix
path: ${{ runner.temp }}/fix.patch
if-no-files-found: ignore
open-draft-pr:
runs-on: ubuntu-latest
needs: propose-fix
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.workflow_run.head_sha }}
- uses: actions/download-artifact@v8
continue-on-error: true
with:
name: codex-fix
path: ${{ runner.temp }}/codex-fix
- env:
GH_TOKEN: ${{ github.token }}
BASE_BRANCH: ${{ github.event.workflow_run.head_branch }}
RUN_URL: ${{ github.event.workflow_run.html_url }}
run: |
PATCH="$RUNNER_TEMP/codex-fix/fix.patch"
[ -s "$PATCH" ] || { echo "Codex proposed no fix"; exit 0; }
if git apply --numstat "$PATCH" | cut -f3 | grep -q '\.github/'; then
echo "Patch touches .github/; refusing" >&2; exit 1
fi
git apply --check "$PATCH" && git apply "$PATCH"
git switch -c "codex/fix-$GITHUB_RUN_ID"
git add -A
git -c user.name="codex-ci" -c user.email="codex-ci@users.noreply.github.com" commit -m "fix: repair CI failure from $RUN_URL"
git push origin HEAD
gh pr create --draft --base "$BASE_BRANCH" --title "Codex: fix CI failure on $BASE_BRANCH" --body "Proposed by Codex for $RUN_URL. Review the diff and CI before merging."

A plain step builds the patch after Codex finishes, and the open-draft-pr job refuses any patch that touches .github/ (--no-renames makes a file renamed into .github/ show up under its new path, so the check sees it), so the job holding a write token never executes anything Codex wrote. The prompt is written into the workflow file on purpose. A workflow_run workflow always runs from the default branch, so that prompt cannot be changed by the branch that failed, while a prompt-file would be read from that branch’s checkout. The event == 'push' filter keeps fork PRs out: they would put untrusted code into a job that holds your OpenAI key. Be clear about what remains: npm ci and the tests Codex runs execute the failing branch’s code in the job that holds that key. That is acceptable only because the filter limits it to branches pushed to this repository, and anyone who can push a branch here can already reach repository secrets through a push-triggered workflow. Do not widen the filter to pull requests or forks.

The action has two separate controls, and you need both. safety-strategy limits the Codex process on the runner. The permission profile limits the commands Codex runs. OpenAI’s security guide says profiles “do not replace the action’s safety-strategy”.

safety-strategyWhat it doesUse it when
drop-sudo (default)Removes sudo for the runner user before Codex starts; on Linux it also removes access to root-owned sockets under /run, including Docker. Irreversible; on a reused self-hosted runner the changes outlive the jobGitHub-hosted Linux and macOS runners, with the action as the last step of its job
unprivileged-userRuns Codex as the account named in codex-userSelf-hosted runners with a pre-created account
read-onlyForces the legacy read-only sandbox; Codex still runs as the default user, which may have sudo and could read the key from memoryRarely; it cannot be combined with a permission profile
unsafeNo privilege reductionWindows runners only, where it is the only option, and only with prompts you fully trust

For the command side, set permission-profile to :read-only for review, gate, and notes jobs, and :workspace for jobs that edit the checkout. The older sandbox input still works, but the README says to prefer a profile for new workflows, and the two are mutually exclusive. Omitting both keeps the legacy workspace-write sandbox, which is wider than a review needs. Extra codex exec flags go through codex-args; the action rejects flags that would weaken permissions, trust or the model provider. A codex-home directory is loaded as trusted configuration without that check, so never point it at files from the checkout under review.

Who can trigger a run matters as much. By default only users with write access to the repository can run the action. allow-users adds named users, allow-bots admits github-actions[bot], and allow-bot-users lists other trusted bots explicitly (it does not accept *). Setting allow-users: "*" on a public repository invites strangers to spend your API quota. If you suspect the key leaked, revoke it in the OpenAI platform, create a new one, and replace the secret.

How do you verify Codex CI output without reading every line?

Section titled “How do you verify Codex CI output without reading every line?”

Treat Codex in CI as one more check whose accuracy you measure, not as a reviewer you trust.

  1. Start advisory. Run the review workflow as a non-required check for a few weeks. Ask reviewers to react to each Codex comment with a thumbs-up (real issue) or thumbs-down (noise). Count both, and write down the precision you need before findings may block a merge.
  2. Gate only on the structured job. When precision meets your bar, first remove the workflow’s paths: filter (see the failure mode below), then make the scan and gate jobs, not the free-text review, required status checks in branch protection. Require both: when scan fails, gate is skipped, and branch protection accepts a skipped check. The gate fails on a schema-checked P0, which is a rule you can audit, instead of a paragraph someone has to interpret.
  3. Keep deterministic gates first. Codex findings add to lint, type checks, and tests; they never replace them. The auto-fix PR is a draft that must pass the same CI and a human approval, and the release stays a draft until a person publishes it.
  4. Keep the evidence. Set output-file and upload it with actions/upload-artifact and a retention-days value, so you can audit what Codex said when a missed bug reaches production.
  5. Name the owners. The code owner of .github/codex/ signs off on prompt and schema changes; the PR’s human approver signs off on the merge.

For the reviewer’s side of the same idea, see reviewing an agent’s PR without reading every line and reading evidence instead of code.

What breaks when Codex runs in GitHub Actions?

Section titled “What breaks when Codex runs in GitHub Actions?”

Only one of `prompt` or `prompt-file` may be specified. You set both inputs. Keep prompt-file for versioned prompts and prompt for inline prompts in workflow_run workflows. The opposite error, Either `prompt` or `prompt-file` must be specified., means neither is set.

Failed to read server info from …. The openai-api-key input was empty, so the action skipped the proxy steps and had no server info to read. Check the secret name. On PRs from forks, GitHub does not pass secrets at all, so the review workflow cannot run there; review fork PRs by hand or with a trusted maintainer re-pushing the branch.

responses-api-proxy did not write server info. The proxy that holds your API key started but did not write its port file within 10 seconds. Re-run the job, and check that the runner can reach the Responses API endpoint (and the responses-api-endpoint URL, if you set one).

Actor '<login>' must have write access to <owner>/<repo>. Detected permission: '<level>'. The user or bot that triggered the run does not have write access to the repository. The variant Actor '<login>' is not a collaborator on <owner>/<repo>; write access is required. means the account is not a collaborator at all. Add the account to allow-users or allow-bot-users only if you trust it with your API key.

`permission-profile` and `sandbox` are mutually exclusive. Remove the sandbox input. The same conflict applies to safety-strategy: read-only combined with a profile.

Expected sudo to be disabled, but sudo succeeded. An earlier step restored sudo, or the runner does not allow the change. On Linux you also cannot run the action a second time with a prompt in the same job after drop-sudo; put the second Codex run in a new job.

bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted on a self-hosted runner. The permission profiles use bubblewrap, which needs unprivileged user namespaces. The action enables them on GitHub-hosted Linux runners; on self-hosted runners you must enable them yourself. Test on a disposable host, because the drop-sudo changes happen before this error and can outlive the job.

Codex cannot read the checkout with unprivileged-user. /home/runner has permissions 750, so the Codex account cannot reach the checkout under /home/runner/work. In a step before the action, add the account to the runner group and give its group read access to $GITHUB_WORKSPACE, as the action’s examples/unprivileged-user.yml does.

The auto-fix PR shows no CI checks. Pushes and PRs made with GITHUB_TOKEN do not start new workflow runs, so CI never runs on the draft. Push with a GitHub App installation token instead, or have the reviewer push a commit to the branch to start CI.

The required security check never reports on a PR. A workflow skipped by its paths: filter leaves its required checks waiting, which blocks the merge. Drop the workflow-level paths: filter before you make the check required, and let the prompt return an empty findings array for PRs that touch none of the sensitive paths.

A comment on every push floods the PR. The concurrency block in the review workflow cancels the in-progress run when a new commit arrives, and the draft == false condition skips drafts until ready_for_review.

Reviews take too long or cost too much. Set timeout-minutes on each Codex job, restrict triggers with paths:, and keep the review non-required until it earns its place. API usage bills to the key, so track it as in Codex cost management.