CI/CD with Codex
How OpenAI's codex-action runs Codex agents in GitHub Actions with a layered security model that sandboxes the agent, drops privileges, and protects API keys from exfiltration.
| Repository | openai/codex-action |
| Package | @openai/codex (CLI), @openai/codex-responses-api-proxy (proxy) |
| Action | openai/codex-action (composite) |
| Runtime | Node.js 20, Codex CLI (@openai/codex) |
| Providers | OpenAI Responses API, Azure |
| Trigger surface | Any GitHub event (issue_comment, pull_request, workflow_dispatch, schedule) |
| Safety strategies | drop-sudo, unprivileged-user, read-only, unsafe |
| License | Apache 2.0 |
How the action works
openai/codex-action is a composite GitHub Action that installs the Codex CLI, starts a local Responses API proxy, and runs codex exec against a user-supplied prompt. It produces a single output — final-message — that downstream jobs can consume.
The execution flow runs through seventeen sequential steps, grouped into four phases:
Phase 1 — Authorization. The action checks whether the triggering GitHub actor has permission to run it. By default, only users with write access to the repository pass this check. The allow-users, allow-bots, and allow-bot-users inputs expand the allowlist.
Phase 2 — Setup. Node.js 20 is provisioned, the Codex CLI and Responses API proxy are installed globally via npm, and the Codex home directory is resolved. If the unprivileged-user safety strategy is selected, a shared home directory is created under that user’s $HOME.
Phase 3 — Proxy. The Responses API proxy starts on a local port with the API key piped in via stdin. The proxy clears the key from its own environment before accepting connections. A config.toml is written in the Codex home directory pointing the model provider at http://127.0.0.1:<port>/v1.
Phase 4 — Execution. If the drop-sudo strategy is active, sudo privileges are irreversibly removed from the runner user. Then codex exec runs with the prompt, sandbox level, model, and any extra arguments. The final message is captured from the output file and exposed as the final-message output.
Safety strategies: how Codex is sandboxed
The safety-strategy input controls the OS-level privilege context Codex runs under. Four strategies are available, and the default is drop-sudo.
drop-sudo (default). The action removes the runner user from the sudo group before Codex starts. It re-enters itself under sudo to strip the user from the sudo/admin group, cleans any user entries in /etc/sudoers and /etc/sudoers.d/, and verifies sudo is gone by running sudo -n true and expecting failure. This is irreversible for the rest of the job.
The reason this matters goes beyond preventing sudo rm -rf /. Even with a read-only sandbox, a user with sudo can read the proxy process memory via /proc/<pid>/mem — which contains the API key in plaintext. The example workflow at examples/test-sandbox-protections.yml demonstrates this exact attack: it greps procfs memory dumps for an API key pattern and succeeds when sudo is available, even with the read-only sandbox active.
unprivileged-user. Codex runs as a dedicated system user with no sudo access. This requires pre-setup: the user must exist, checkout files must be group-readable by that user, and the setgid bit must be set on directories so new files inherit the group. The action handles Codex home directory creation under the unprivileged user’s $HOME, and wraps codex exec with sudo -u <user> --.
read-only. The Codex sandbox is set to read-only, preventing filesystem writes and network access. However, this does not protect the API key — the proxy process still holds it in memory, and a sudo-capable user can extract it. Use this only when API key exposure is not a concern or when combined with another mitigation.
unsafe. No sandbox, no privilege drop. Required on Windows runners, which lack sandbox support. The action enforces this: if the OS is Windows and safety-strategy is anything other than unsafe, the action fails immediately.
| Strategy | Sudo removed | API key protected | Sandbox enforced | Works on Windows |
|---|---|---|---|---|
drop-sudo | Yes | Yes | Configurable | No |
unprivileged-user | Yes (separate user) | Yes | Configurable | No |
read-only | No | No | Yes (forced) | No |
unsafe | No | No | No | Yes (required) |
Permission checks: who can trigger the agent
Before any code runs, the action verifies the triggering actor is authorized. The check is implemented in checkActorPermissions.ts and follows a strict precedence chain.
If allow-users is set to *, all actors pass. If it is a comma-separated list, the actor must appear in the list (case-insensitive). If neither applies, the action calls the GitHub API — repos.getCollaboratorPermissionLevel — and requires admin, write, or maintain permission.
Bot actors get special handling. github-actions[bot] can be trusted via allow-bots: true, but only that specific bot — the hardcoded trusted set excludes dependabot[bot], renovate[bot], and all other bots. Additional bots can be allowlisted with allow-bot-users, which accepts a comma-separated list but explicitly rejects * as a wildcard. Bot allowlists do not apply to human actors — a human named renovate (without [bot]) won’t match allow-bot-users: renovate.
API key protection: keeping secrets off the runner
The action takes a layered approach to API key handling. The key is not passed as a CLI argument or environment variable to the proxy — it is piped to stdin using printenv PROXY_API_KEY | env -u PROXY_API_KEY codex-responses-api-proxy. The env -u flag removes the variable from the proxy’s process environment before it starts, so no copy persists in /proc/<pid>/environ.
After the proxy starts, the server info file — which contains the proxy’s PID — is locked down: sudo chown root and sudo chmod 444. This prevents the runner user from finding the proxy process to read its memory. Combined with drop-sudo, the PID is visible but the memory is not reachable.
The proxy configuration is written to $CODEX_HOME/config.toml, pointing Codex at http://127.0.0.1:<port>/v1 with the wire API set to responses. Codex never sees the API key directly — it only communicates with the local proxy over HTTP.
name: Codex PR Review
on:
pull_request:
types: [opened, synchronize]
permissions:
contents: read
pull-requests: write
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
- uses: openai/codex-action@v1
id: codex
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
prompt: |
Review this PR diff for bugs, security issues, and code quality problems.
PR title: ${{ github.event.pull_request.title }}
Base ref: ${{ github.event.pull_request.base.ref }}
Head ref: ${{ github.event.pull_request.head.ref }}
Report findings as a structured code review with severity levels.
- name: Post review comment
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const output = `${{ steps.codex.outputs.final-message }}`;
await github.rest.issues.createComment({
...context.repo,
issue_number: context.issue.number,
body: output,
}); The ref: refs/pull/<number>/merge checkout is load-bearing — it ensures Codex sees the merged state, not just the head branch in isolation. The review posting step runs in the same job because final-message needs to be consumed as a job output. For production use, run the review posting in a separate job via needs and job-level outputs to satisfy the post-execution hygiene recommendation.
When to use which safety strategy
| Scenario | Strategy |
|---|---|
| PR review from trusted collaborators | drop-sudo |
| Public repo accepting PRs from anyone | unprivileged-user with tight allow-users |
| Analysis-only pipeline (no writes needed) | read-only (with drop-sudo for key protection) |
| Windows runner | unsafe |
| Maximum isolation for untrusted prompts | unprivileged-user with dedicated system user |
Takeaways
A single action with a layered security model
openai/codex-action is one composite action, not a suite. Its security comes from four independent layers — actor authorization, safety strategy, API key protection, and post-execution hygiene — each of which can be configured independently.
drop-sudo is the default for a reason
Removing sudo privileges before Codex runs prevents reading the proxy process memory via /proc/<pid>/mem, which would otherwise expose the API key even with a read-only sandbox.
The proxy strips the API key from its own environment
codex-responses-api-proxy receives the API key via stdin and clears it from its process environment with env -u. Combined with drop-sudo, the key is inaccessible to both the sandboxed agent and a compromised runner user.
Run codex-action as the last step in a job
Codex can spawn lingering processes, modify .git/hooks, or overwrite other actions' source. Post-processing steps — like posting PR comments — belong in a separate job that consumes the final-message output.