Run Codex non-interactively with codex exec
codex exec runs Codex without the interactive terminal UI: it prints progress to stderr and the agent’s final message to stdout, never stops to ask for approval, and exits non-zero when the run errors. It is the building block for scripts, cron jobs and CI steps, provided the calling script, not the agent, checks the result.
This page is for developers and tech leads who already use Codex interactively and now want it inside a pipeline. The usual situation: a nightly job should triage failing tests, a pre-merge step should return a machine-readable verdict, or a release script needs notes in a fixed shape. A chat transcript is useless to a shell script. You need exact flags, output a parser can trust, and a gate that fails loudly when the agent claims success it did not earn.
What this guide covers
Section titled “What this guide covers”- The
codex execinvocations for read-only analysis, workspace edits, schema-checked JSON and multi-stage runs, checked against Codex CLI 0.157.1. - A strict JSON Schema and
jqrecipes that turn the JSONL event stream into pass/fail signals. - A GitHub Actions job that runs Codex on a trusted branch, keeps the API key away from the agent’s shell, and verifies the change without anyone reading the diff line by line.
- A failure-modes list with the exact error text and the fix for each.
How does codex exec differ from interactive Codex?
Section titled “How does codex exec differ from interactive Codex?”The same agent runs underneath, but the defaults change because no one is at the keyboard. These behaviours were read from the codex exec --help output and the codex-rs/exec source of Codex CLI 0.157.1.
| Behaviour | Interactive codex | codex exec |
|---|---|---|
| Output | Full-screen terminal UI | Progress on stderr, final message on stdout; --json switches stdout to JSONL events |
| Approvals | Asks according to approval_policy | Never asks. A command that needs approval outside the sandbox is refused instead |
| Sandbox | From config.toml or /permissions | From --sandbox, -c default_permissions=… or config.toml. Read-only unless you grant more |
| Git check | No Git requirement | Exits 1 outside a Git repository unless you pass --skip-git-repo-check |
| Session files | Saved, resumable with codex resume | Saved, resumable with codex exec resume, unless you pass --ephemeral |
| Prompt source | Typed | Argument, - for stdin, or both: piped stdin is appended to the prompt as a <stdin> block |
codex exec also has an alias, codex e, and a sibling for reviews, codex exec review, covered later on this page.
Run your first headless task
Section titled “Run your first headless task”-
Authenticate. On your own machine,
codex loginis enough;codex execreuses the stored login. In CI, setCODEX_API_KEYfrom a secret store instead.codex execreads that variable directly, so no credential file is written to the runner. See Codex authentication for the sign-in options. -
Start read-only. With no sandbox flag, Codex can read the repository and run read-only commands, but it cannot edit files.
Terminal window codex exec "List the five modules with the most complex control flow and say why each is risky." -
Feed it context through a pipe. When stdin is piped and a prompt is given, Codex appends stdin to the prompt. Use this for diffs, logs and test output.
Terminal window git diff origin/main...HEAD | codex exec "Summarise this diff as release notes: Features, Fixes, Breaking changes." -
Grant write access only when the task edits files.
--sandbox workspace-writelets Codex edit the working tree; network access stays off.Terminal window codex exec --sandbox workspace-write "Fix the failing test in tests/billing/invoice.test.ts. Change only src/billing/." -
Capture the answer in a file.
-o(--output-last-message) writes the agent’s final message to a file, so the script does not have to scrape stdout.Terminal window codex exec -o triage.md "Read the last CI log in ci.log and name the root cause in three sentences."
In CI, also pin the model with -m so a change of default does not silently alter a nightly job’s behaviour. The current default model and its effort levels are listed on the models hub; set effort per run with -c model_reasoning_effort="high" when a task needs it.
Choose the permissions for an unattended run
Section titled “Choose the permissions for an unattended run”Pick the narrowest option that lets the task finish. Because codex exec never asks, the choice you make here is the whole security decision.
| Option (Codex CLI 0.157.1) | What Codex can do | Use it for |
|---|---|---|
No flag (read-only) | Read files, run read-only commands | Triage, analysis, reviews, reports |
--sandbox workspace-write | Edit the working tree and --add-dir paths; no network | Fixes, refactors, generated files |
-c default_permissions=":workspace" | A permission profile, the approach OpenAI now prefers for new integrations (beta, CLI 0.138.0 or later) | New pipelines; do not combine with --sandbox or --approve-for-me, because OpenAI states the two systems “do not compose” |
--approve-for-me | Routes approval requests to the automatic reviewer, inside the workspace-write sandbox | Jobs where a boundary request is expected and a model may decide it |
--dangerously-bypass-approvals-and-sandbox | Everything, with no sandbox | Only inside a disposable, network-restricted container that is itself the boundary |
Every example on this page uses the legacy --sandbox workspace-write rather than a permission profile, for two reasons: profiles are still beta in 0.157.1, and OpenAI states that the profile and legacy sandbox systems “do not compose”, so one command should use one system only. A new pipeline can adopt profiles by replacing the flag with -c default_permissions=":workspace" in every command; do not keep both.
Two more flags make a CI run reproducible. --ignore-user-config skips $CODEX_HOME/config.toml, so a runner’s leftover MCP servers or model settings cannot change the job. --strict-config fails the run when config.toml contains a key this Codex version does not recognise, which catches a typo before it becomes a silent default. Configuration keys and layering are explained in Codex configuration.
Get JSON output your pipeline can parse
Section titled “Get JSON output your pipeline can parse”--output-schema takes a JSON Schema file and constrains the agent’s final message to it. Codex sends the schema with strict validation switched on (strict: true in the 0.157.1 source), so the schema itself has to follow strict-mode rules: every object lists all its properties in required and sets "additionalProperties": false, at every level. Express an optional field as a nullable type instead of leaving it out of required.
Save this as .github/codex/risk-schema.json:
{ "type": "object", "properties": { "verdict": { "type": "string", "enum": ["pass", "fail"] }, "findings": { "type": "array", "items": { "type": "object", "properties": { "file": { "type": "string" }, "line": { "type": ["integer", "null"] }, "severity": { "type": "string", "enum": ["high", "medium", "low"] }, "evidence": { "type": "string" } }, "required": ["file", "line", "severity", "evidence"], "additionalProperties": false } } }, "required": ["verdict", "findings"], "additionalProperties": false}Then run it and let jq -e turn the answer into an exit code:
codex exec --output-schema .github/codex/risk-schema.json -o risk.json \ "$(cat .github/codex/risk-prompt.md)"jq -e '.verdict == "pass" and ([.findings[] | select(.severity == "high")] | length == 0)' risk.jsonThe schema guarantees the shape, not the truth. A "verdict": "pass" means the agent believed it; the tests and checks in the verification section below decide whether you do.
Read the JSONL event stream
Section titled “Read the JSONL event stream”--json replaces the human-readable stdout with one JSON object per line. In Codex CLI 0.157.1 the top-level type values are thread.started, turn.started, turn.completed, turn.failed, item.started, item.updated, item.completed and error. Items carry their own type: agent_message, reasoning, command_execution, file_change, mcp_tool_call, collab_tool_call, web_search, todo_list and error.
An abridged run looks like this (values are illustrative):
{"type":"thread.started","thread_id":"0199a213-81c0-7800-8aa1-bbab2a035a53"}{"type":"turn.started"}{"type":"item.completed","item":{"id":"item_1","type":"command_execution","command":"npm test","aggregated_output":"...","exit_code":1,"status":"failed"}}{"type":"item.completed","item":{"id":"item_4","type":"agent_message","text":"The invoice test fails because rounding happens before tax."}}{"type":"turn.completed","usage":{"input_tokens":24763,"cached_input_tokens":19200,"cache_write_input_tokens":0,"output_tokens":122,"reasoning_output_tokens":64}}Save the stream once, then query it for what the pipeline needs:
codex exec --json "Diagnose the failing tests in ci.log" > events.jsonl
# The session ID, for a later resumejq -r 'select(.type == "thread.started") | .thread_id' events.jsonl
# The final answerjq -r 'select(.type == "item.completed" and .item.type == "agent_message") | .item.text' events.jsonl | tail -n 1
# Every command the agent ran that exited non-zero# (declined commands carry exit_code null, so the null check skips them)jq -r 'select(.type == "item.completed" and .item.type == "command_execution" and .item.exit_code != null and .item.exit_code != 0) | .item.command' events.jsonl
# Which files it changedjq -r 'select(.type == "item.completed" and .item.type == "file_change") | .item.changes[].path' events.jsonl
# Output tokens for the whole run, for cost trackingjq -s '[.[] | select(.type == "turn.completed") | .usage.output_tokens] | add' events.jsonlKeep events.jsonl as a build artifact. It is the audit trail of what the agent actually ran, which matters more than what its final message says it ran.
Chain stages with resume and fork
Section titled “Chain stages with resume and fork”Split long work into stages so that each stage has one job and your script can check it before the next one starts. codex exec resume continues a saved session; codex exec fork copies one into a new session, so you can try two approaches from the same analysis.
# Stage 1: analyse, read-only, and record the session IDcodex exec --json "$(cat plan-prompt.md)" > stage1.jsonlTHREAD_ID=$(jq -r 'select(.type == "thread.started") | .thread_id' stage1.jsonl)
# Stage 2: implement in the same session, now with write accesscodex exec --sandbox workspace-write resume "$THREAD_ID" \ "Implement step 1 of your plan only. Run the tests you listed. Stop after that."
# Alternative: branch the analysis into a separate session, also with write accesscodex exec --sandbox workspace-write fork "$THREAD_ID" \ "Implement the plan with the smallest possible diff instead."Note where --sandbox goes. Neither codex exec resume nor codex exec fork has a --sandbox option of its own in 0.157.1 (unexpected argument '--sandbox' found), so the flag sits before the subcommand. A fork without it inherits the read-only default and cannot edit anything. Prefer the explicit session ID over resume --last: --last picks the newest session recorded for the current directory, which is the wrong one as soon as two jobs share a runner. A run started with --ephemeral writes no session files, so it cannot be resumed at all.
Review a diff headlessly with codex exec review
Section titled “Review a diff headlessly with codex exec review”For review-only jobs, codex exec review runs Codex’s built-in reviewer. You give it either a preset target (uncommitted changes with --uncommitted, a base branch with --base, or one commit with --commit) or custom review instructions as the prompt, not both. In Codex CLI 0.157.1, --base, --uncommitted and --commit cannot be combined with custom review instructions: the command fails at argument parsing with cannot be used with '[PROMPT]'.
A preset target, with the built-in review instructions:
codex exec review --base main --json -o review.md > review-events.jsonlCustom instructions, with the diff range named inside the instructions instead of in a flag:
codex exec review -o review.md "$(cat .github/codex/review-prompt.md)"Both forms accept --json and -o. The -o file is a human-readable report: an overall explanation followed by prioritised findings. In 0.157.1 the built-in reviewer runs under its own rubric and fixed output format, so your instructions change what it looks for, not the shape of what it returns. Do not grep that report for a pass or fail line; nothing guarantees one. --output-schema is accepted at parse time, but in the 0.157.1 source the review request carries no schema. Any machine gate belongs on a plain codex exec with --output-schema and the same review prompt, as in the JSON output section; use codex exec review for the report a human reads.
How review fits into a team’s pull request flow, including GitHub-side review, is covered in code review with Codex.
Verify the result without reading every line
Section titled “Verify the result without reading every line”The agent’s final message is a claim. The pipeline’s job is to check it with signals the agent cannot fake, and to hand a human a short evidence summary rather than a raw diff. Run these checks in the script after codex exec returns, as separate steps:
- Exit code.
codex execexits 1 when the run errors (bad schema file, Git check, failed turn). Treat that as a hard failure. Exit 0 only means the run finished, not that the task succeeded. - Re-run the tests yourself. Do not trust
"tests pass"in the output. Run the suite in a fresh step with the job’s own command. If the agent edited a test to make it pass, the next check catches it. - Protected paths. Write
events.jsonland the-ofile outside the checkout (for example under$RUNNER_TEMP), so they are not staged with the agent’s change and do not count against the diff budget. Stage everything withgit add -A(so new files count), then fail the job if the change touches files the agent must not edit:git diff --cached --name-only | grep -qE '^(tests/|\.github/|migrations/|package(-lock)?\.json$)' && exit 1.package.jsonis on the list because the agent could otherwise redefine thetestscript that the next check runs. - Diff budget. Cap the change size so a “one-line fix” cannot quietly become a rewrite:
git diff --cached --numstat | awk '{s+=$1+$2} END {exit (s > 200)}'. - Schema verdict.
jq -eon the-ofile, as shown above. - Evidence for the human. Attach
events.jsonl, the-ofile and the test output to the pull request. The reviewer checks that the evidence matches the intent; the evidence bundle page defines what that pull request must prove.
Who signs off stays the same as for human changes: the code owner of the touched paths approves the pull request, and nothing the job produces merges without it.
Run codex exec in CI with an API key
Section titled “Run codex exec in CI with an API key”In GitHub Actions, the official openai/codex-action@v1 is the first choice: it keeps the key behind a proxy and can drop sudo before Codex starts. Its inputs are covered in CI/CD with the Codex GitHub Action. Call codex exec directly when you need a flag the action does not expose, or when you run on another CI system.
Two rules make the direct call safe. First, run it only on trusted code: a schedule or manual trigger on the default branch, never on a contributor’s ref while a secret is in the environment. Second, keep the key out of the agent’s shell. In Codex CLI 0.157.1 the commands Codex runs inherit the full environment by default (shell_environment_policy.ignore_default_excludes defaults to true). Setting it to false filters out every variable whose name contains KEY, SECRET or TOKEN.
name: codex-nightly-test-fixon: schedule: - cron: '17 3 * * 1-5' workflow_dispatch:
permissions: contents: read
jobs: fix: runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v7 with: persist-credentials: false - run: npm ci # install before Codex runs: workspace-write has no network - run: npm install -g @openai/codex@0.157.1 - name: Run Codex env: CODEX_API_KEY: ${{ secrets.OPENAI_API_KEY }} CODEX_MODEL: gpt-6-astra run: | # Model pinned on purpose: the bundled default changes between CLI releases codex exec -m "$CODEX_MODEL" --sandbox workspace-write --ephemeral --ignore-user-config \ -c shell_environment_policy.ignore_default_excludes=false \ --json --output-schema .github/codex/fix-schema.json -o "$RUNNER_TEMP/fix.json" \ "$(cat .github/codex/fix-prompt.md)" > "$RUNNER_TEMP/events.jsonl" - name: Verify independently run: | STATUS=$(jq -r '.status' "$RUNNER_TEMP/fix.json") git add -A if [ "$STATUS" = "nothing_to_fix" ]; then # A claim, not a pass: the tree must be untouched and the suite green. git diff --cached --quiet || { echo "nothing_to_fix, yet files changed"; exit 1; } npm test exit 0 fi if [ "$STATUS" != "fixed" ]; then echo "Codex status: $STATUS"; exit 1; fi if git diff --cached --name-only | grep -qE '^(tests/|\.github/|migrations/|package(-lock)?\.json$)'; then echo "Codex changed a protected path"; exit 1 fi git diff --cached --numstat | awk '{s+=$1+$2} END {exit (s > 200)}' npm test git diff --cached > "$RUNNER_TEMP/codex.patch" - uses: actions/upload-artifact@v7 if: always() with: name: codex-run path: | ${{ runner.temp }}/events.jsonl ${{ runner.temp }}/fix.json ${{ runner.temp }}/codex.patchThe verify step treats nothing_to_fix as a claim too: it fails if the tree changed and still runs the suite. The job has read-only repository permissions and produces a patch artifact; opening the pull request is a separate, reviewed step described in CI/CD with the Codex GitHub Action. .github/codex/fix-schema.json follows the strict rules above, with status ("fixed", "nothing_to_fix", "not_fixed" or "needs_human"), summary, files_changed and tests_run, all required.
When codex exec fails in a script
Section titled “When codex exec fails in a script”Not inside a trusted directory and --skip-git-repo-check was not specified. The working directory is not a Git repository. Run from the checkout root, or use -C <dir> to point at it. Add --skip-git-repo-check only for a scratch directory you created yourself, because without Git you lose the diff that the verification steps depend on.
Output schema file … is not valid JSON, or the API rejects the schema. The first is a syntax error in the file; validate it with jq . schema.json. The second is almost always strict mode: a property missing from required, or an object without "additionalProperties": false. Make every property required and use ["string", "null"] for optional values.
error: unexpected argument '--full-auto' found or unexpected argument '--sandbox' found. --full-auto is gone (see the caution above). --sandbox after resume or fork is not accepted; move it before the subcommand: codex exec --sandbox workspace-write resume <SESSION_ID> "…".
error: the argument '--base <BRANCH>' cannot be used with '[PROMPT]'. In 0.157.1, codex exec review takes a preset target (--base, --uncommitted, --commit) or custom review instructions, never both; the same error appears for --uncommitted and --commit. Drop the flag and name the diff range in the instructions, or drop the instructions and use the built-in review.
The agent reports that it could not write a file or install a package. The run is read-only by default, and workspace-write still has no network. Grant --sandbox workspace-write for edits (before resume or fork in a multi-stage run), and install dependencies in a step before codex exec. Do not reach for --dangerously-bypass-approvals-and-sandbox to get past this on a shared runner.
resume --last continues the wrong session. --last means the newest session for this directory, and parallel jobs on one runner race for it. Capture thread_id from the thread.started event and resume by ID. If the first stage used --ephemeral, there is nothing to resume; drop the flag on stages you plan to continue.
Exit code 0, but the task is not done. The exit code reports run errors, not task success. Gate on the schema verdict, the independent test run and the diff checks from the verification section, never on exit status alone.
The job behaves differently on a runner than on your laptop. A config.toml in the runner’s CODEX_HOME is adding MCP servers, a different model or rules. Add --ignore-user-config, pin the model with -m, and use --ignore-rules if user or project execpolicy .rules files should not apply to this job.
Hooks you expect to run do not. Codex hooks need persisted trust. --dangerously-bypass-hook-trust runs them without it, which is acceptable only when the job itself vets where the hooks come from, never for hooks in a contributor’s branch.
Where to go next with headless Codex
Section titled “Where to go next with headless Codex”- CI/CD with the Codex GitHub Action: the official action, its safety strategies and the step that opens the pull request.
- Codex SDK: drive the same agent from TypeScript or Python when a shell script is no longer enough.
- Codex automations: scheduled tasks that run inside the Codex app instead of your CI.
- Codex cost management: turn the
usagenumbers from the JSONL stream into a budget. - The evidence bundle: what an agent’s pull request must prove before a human approves it.
- Claude Code automation: the equivalent headless workflow with
claude -p, if your team runs both tools.