All articles
agentic systems·advanced··Updated

How codex-action sandboxes Codex in GitHub Actions

How codex-action separates command permissions, host privileges, proxy key handling, actor authorization, and post-execution isolation on a GitHub runner.

automationci-cdcodexgithub-actionssecurity
Resources
Repository openai/codex-action
Security guide Security
Action definition action.yml
Command policy Codex permissions

permission-profile and safety-strategy protect different boundaries. The first constrains filesystem and network access; the second strips host privileges. codex-action adds actor authorization, proxy key handling, and a separate post-processing job around them.

At a glance
Action openai/codex-action (composite)
Runtime Node.js 24, Codex CLI (@openai/codex)
Providers OpenAI Responses API, Azure
Trigger surface Any GitHub event (issue_comment, pull_request, workflow_dispatch, schedule)
Command policy Built-in or configured permission-profile; legacy sandbox fallback
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 composite action’s steps group into four responsibilities:

Phase 1 — Runtime and authorization. The action validates the Windows safety strategy, provisions Node.js 24, and checks whether the triggering GitHub actor may continue. By default, only users with write access to the repository pass; allow-users, allow-bots, and allow-bot-users expand the allowlist.

Phase 2 — Tool setup. 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. The action resolves a permission profile or legacy sandbox before constructing codex exec. With the default drop-sudo strategy, it then revokes sudo; Linux also clears supplementary groups and capabilities, enables no_new_privs, and removes access to writable root-owned service sockets. The final message becomes the final-message output.

Loading diagram…

Separate command permissions from host privileges

New workflows should select the narrowest permission-profile that completes the task. :workspace permits edits inside the checkout without network access; :read-only is the analysis-only profile. A named profile in trusted codex-home/config.toml can narrow the policy further.

Permission profiles require Codex CLI 0.138.0 or later. A workflow that pins an older codex-version must use the legacy sandbox model instead.

Permission profiles do not compose with the legacy sandbox input. The action rejects both together, and safety-strategy: read-only also forces the legacy read-only sandbox. Secure read-only analysis therefore pairs safety-strategy: drop-sudo with permission-profile: ":read-only".

Treat codex-home as workflow configuration, not repository input. The action validates dangerous codex-args, but it does not sanitize a checked-in config.toml or named profile.

The safety-strategy input controls the OS-level privilege context. Four strategies are available, and the default is drop-sudo.

drop-sudo (default). The action removes the runner user from the sudo/admin group, cleans user entries from /etc/sudoers and /etc/sudoers.d/, and verifies that passwordless sudo fails. On Linux, Codex starts through setpriv with no_new_privs, no supplementary groups, and empty capability sets; writable root-owned service sockets such as Docker’s are removed from the user’s reach. These mutations last for the rest of the job and can outlive a job on reused self-hosted runners.

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 pre-created system user with no sudo access. The workflow must make the checkout and required files readable to that account; the action resolves its Codex home and launches codex exec as that user.

read-only. This legacy strategy forces the read-only sandbox, preventing filesystem writes and direct network access, but it does not remove sudo. A sudo-capable runner user can still read the proxy process memory and recover the API key. Use drop-sudo with permission-profile: ":read-only" when the key must remain protected.

unsafe. No sandbox and no privilege reduction. Windows runners require it because the action has no supported Windows sandbox; every other strategy fails during validation.

Safety strategy comparison
Strategy Host privilege reduction API key protected Command policy Works on Windows
drop-sudo Sudo, groups, Linux capabilities, service sockets Yes Permission profile or legacy sandbox No
unprivileged-user Dedicated account Yes Permission profile or legacy sandbox No
read-only None No Legacy read-only sandbox, forced No
unsafe None No None Yes, required

Permission checks: who can trigger the agent

After provisioning Node but before installing Codex or starting the proxy, the action verifies that 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.


Keep the API key out of the agent environment

The action does not pass the key as a CLI argument or leave it in the proxy’s environment. A background shell replaces itself with exec env -u PROXY_API_KEY -u NODE_OPTIONS, sets NODE_OPTIONS=--disable-sigusr1, and supplies the key through stdin. The proxy retains the credential in memory because it must authenticate upstream requests, so process isolation remains necessary.

After the proxy starts, the action makes its server-info file root-owned and read-only with mode 0444. The file remains readable, so this hardening does not hide the proxy PID. drop-sudo or a dedicated unprivileged account is what makes the process memory unreachable.

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.

codex-pr-review.ymlyaml
name: Codex PR Review
on:
  pull_request:
    types: [opened]

jobs:
  codex:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    outputs:
      final_message: ${{ steps.run_codex.outputs.final-message }}
    steps:
      - uses: actions/checkout@v5
        with:
          ref: refs/pull/${{ github.event.pull_request.number }}/merge
          persist-credentials: false

      - name: Fetch base and head refs
        env:
          PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: |
          git fetch --no-tags origin \
            "$PR_BASE_REF" \
            "+refs/pull/$PR_NUMBER/head"

      - uses: openai/codex-action@v1
        id: run_codex
        with:
          openai-api-key: ${{ secrets.OPENAI_API_KEY }}
          permission-profile: ":workspace"
          prompt: |
            Review only the changes introduced by this pull request.
            Report concrete bugs and security issues with file references.

  post_feedback:
    runs-on: ubuntu-latest
    needs: codex
    if: needs.codex.outputs.final_message != ''
    permissions:
      issues: write
      pull-requests: write
    steps:
      - name: Post review comment
        uses: actions/github-script@v7
        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,
            });

The merge-ref checkout lets Codex inspect the pull request against the base state, and persist-credentials: false keeps the checkout token out of Git configuration. codex-action is the last step in the first job. The second job receives only final-message and owns the write-capable GitHub token.


Match both controls to the workflow

Host strategy and command policy by scenario
Scenario Safety strategy Permission profile
PR review that may edit the checkout drop-sudo :workspace
Analysis-only workflow drop-sudo :read-only
Managed self-hosted runner unprivileged-user Narrow named profile
Windows runner with a trusted prompt unsafe No supported sandbox


Takeaways

Five boundaries protect one agent run

Actor authorization, permission profiles, host privilege reduction, proxy key handling, and a separate job for post-processing address different attack surfaces.

drop-sudo is the default for a reason

Removing sudo, supplementary groups, Linux capabilities, and writable root-service sockets blocks host-level paths that a filesystem sandbox does not cover.

The proxy strips the API key from its own environment

codex-responses-api-proxy receives the API key through stdin and starts without that variable in its environment. drop-sudo protects the remaining in-memory copy from the agent process.

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.