Headless Runs with codex exec
One-shot tasks for scripts, cron, and pipelines
- Explain how `codex exec` (alias `codex e`) differs from the interactive `codex` REPL and why it's the right tool for CI, cron, and scripts
- Use the progress-to-stderr / result-to-stdout split to capture just the final answer with `RESULT="$(codex exec '...')"`
- Choose the correct sandbox for a headless task, starting from the read-only default and opting into `workspace-write` only when edits are required
- Pipe data into Codex using both stdin modes — prompt-plus-stdin and `codex exec -` — and know when each applies
- Apply the core exec flags (`--json`, `--output-last-message/-o`, `--output-schema`, `--sandbox/-s`, `--model/-m`, `--skip-git-repo-check`, `--ephemeral`, `--color`) correctly
- Resume a prior headless run with `codex exec resume <SESSION_ID>` (and `--last` / `--all`) and reason about where sessions persist
`codex exec` is the non-interactive entry point that powers every Codex automation: it runs a single task and exits, streaming progress to stderr while printing only the final answer to stdout. This lesson teaches the headless model end to end — the exec-vs-interactive split, the stdout/stderr contract that makes answers easy to capture, the default read-only sandbox, the two stdin piping modes, the flags you'll actually use, and how to resume a headless session.
- 1The mental model: one shot, then exit
- 2The contract: progress to stderr, answer to stdout
- 3Safety first: the default sandbox is read-only
- 4Feeding data in: the two stdin modes
- 5The flags you'll actually use
- 6Resuming a headless session
The mental model: one shot, then exit
Everything in this automation module rests on a single distinction. Plain codex opens an interactive REPL/TUI — a conversation you steer turn by turn, approving actions as they come. codex exec (alias codex e) does the opposite: it runs one task non-interactively and exits. No prompt, no human in the loop, no waiting for input. You hand it an instruction, it works, it prints a result, and the process returns.
That single property — runs to completion and exits — is why codex exec is the foundation of all Codex automation. A CI job, a cron entry, a Git hook, a shell pipeline: none of these can sit at an interactive prompt waiting for a human to type "yes." They need a command that behaves like every other Unix tool — takes input, does work, produces output, sets an exit code. codex exec is that command.
# Interactive: opens a REPL you drive by hand
codex
# Headless: runs one task and exits — the automation entry point
codex exec "explain what the auth middleware does"
codex e "explain what the auth middleware does" # same thing, shorter aliasKeep the larger Codex picture in mind here. "Codex" today is the 2025-era agent — an open-source CLI written in Rust, plus IDE, cloud, and SDK surfaces that all share one ChatGPT account. It is not the 2021 Codex language model (the GPT-3 variant behind early Copilot, deprecated in 2023). When this lesson says codex exec, it means the modern agent running headlessly — the same engine as the interactive CLI, just without the human at the keyboard.
Key insight
The one-line definition
Interactive codex is a conversation. codex exec is a command — it runs a single task to completion and exits, which is exactly what a pipeline, a cron job, or a CI step needs.
Note
Mind the interrupt key
If you do run an exec interactively and need to stop it, Ctrl+C cancels the turn — Codex uses Ctrl+C, not Esc, to interrupt. (Esc Esc edits the previous message in the interactive TUI.) In a real pipeline, you'd let it run to completion or enforce a timeout instead.
The contract: progress to stderr, answer to stdout
The detail that makes codex exec genuinely pipe-friendly is how it splits its output across the two standard streams:
- While it runs, Codex streams progress to stderr — the reasoning, the commands it executes, the files it touches. This is the live narration you'd watch in a terminal.
- When it finishes, it prints only the final agent message to stdout (or a newline-delimited JSON event stream when you pass
--json).
That clean separation is the whole game. Because the answer lands on stdout and the noise lands on stderr, you can capture exactly the result with ordinary shell command substitution — no log-scraping, no regex, no parsing the chatter:
# RESULT holds ONLY the final message; progress still shows on your terminal via stderr
RESULT="$(codex exec 'in one sentence, what does src/auth.ts export?')"
echo "$RESULT"This is the same Unix discipline behind tools like curl or grep: data on stdout, diagnostics on stderr. It means you can pipe the answer into the next stage of a pipeline, assign it to a variable, or redirect it to a file, while a human (or a CI log) still sees the progress narration separately.
# Send the answer into a file; keep watching progress on the terminal
codex exec 'summarize the changes in this branch' > summary.txt
# Or silence the progress entirely and keep only the result
codex exec 'list the public functions in src/' 2>/dev/nullA practical note on failure: codex exec exits non-interactively, so treat a non-zero exit code as failure (a failed turn surfaces as an error). A precise exit-code table isn't pinned in the public docs, so in scripts, check the exit status and don't assume 0 blindly — verify against codex exec --help on your installed version.
Tip
Capture the answer, keep the narration
RESULT="$(codex exec '...')" grabs just the final message because progress goes to stderr and only the answer goes to stdout. Add 2>/dev/null to drop the progress, or 2>run.log to archive it.
Safety first: the default sandbox is read-only
Before you let an unattended agent loose, you need to know what it's allowed to do. Codex separates two controls that are easy to confuse — keep them distinct:
- Sandbox = WHERE Codex can act (the OS-enforced filesystem and network boundary).
- Approvals = WHEN Codex must stop and ask a human.
These are independent. In headless automation there's no human to approve anything, so the sandbox is your real safety boundary — it's what physically prevents the agent from writing files or reaching the network, regardless of approval policy.
The critical default: codex exec runs read-only. Out of the box it can inspect and reason about your code but cannot edit files or run mutating commands. That's a deliberate safe-by-default posture for automation. To let it change things, you must opt in:
Sandbox (--sandbox / -s) | What the agent can do |
|---|---|
read-only (exec default) | Inspect files only — no edits, no mutating commands |
workspace-write | Edit files in the working directory (network still off by default) |
danger-full-access | No sandbox at all — full filesystem and network access |
# Read-only (default): safe analysis, no changes possible
codex exec 'find any TODO comments and summarize them'
# Opt in to edits for a task that must modify files
codex exec --sandbox workspace-write 'fix the failing lint errors in src/'
codex exec -s workspace-write 'fix the failing lint errors in src/' # -s is the alias
# Equivalent via config override
codex exec --config sandbox_mode="workspace-write" 'fix the lint errors'Use danger-full-access (or its blunt cousin --yolo / --dangerously-bypass-approvals-and-sandbox, which drops both sandbox and approvals) only in trusted, ephemeral environments — a throwaway CI container you're about to destroy — never on a developer's machine or a persistent runner. The deprecated --full-auto flag is a legacy alias; prefer --sandbox workspace-write explicitly.
Watch out
Don't reach for --yolo by reflex
--yolo (alias of --dangerously-bypass-approvals-and-sandbox) removes every safety control — both the sandbox and approvals. Reserve it for disposable, isolated environments. On any machine you care about, grant the narrowest sandbox the task needs, starting from read-only.
Key insight
Sandbox and approvals are orthogonal
Sandbox is WHERE the agent can act; approvals are WHEN it must ask. In headless runs there's no one to ask, so the sandbox is doing the real work of keeping an unattended agent contained. Choose it deliberately for every automation.
Feeding data in: the two stdin modes
Automation usually means handing Codex the output of something else — a failing test log, an error dump, a diff. codex exec supports two distinct ways to pipe data in, and the difference matters.
Mode 1 — prompt-plus-stdin. You give the instruction as the argument, and whatever is piped in becomes additional context appended to that instruction. This is the common case: a fixed task applied to variable input.
# The piped test output is context; the quoted string is the task
npm test 2>&1 | codex exec "summarize the failing tests and propose the smallest likely fix"Mode 2 — codex exec - (the dash sentinel). When the argument is a single -, stdin becomes the entire prompt. Nothing is hard-coded; the whole instruction arrives over the pipe. Use this when the prompt itself is assembled dynamically.
# stdin IS the prompt — assemble it however you like, then feed it in
printf "Summarize this error log in 3 bullets:\n\n%s\n" "$(tail -n 200 app.log)" | codex exec -The mental shortcut: with a quoted task argument, stdin is extra context; with -, stdin is the whole prompt. Mode 1 is what you reach for when the job is fixed and only the input varies ("always summarize whatever test output I pipe you"). Mode 2 is for when your script is composing the instruction from templates, variables, or files at runtime.
Example
Same data, two intentions
cat error.log | codex exec "explain the root cause" → the log is context for a fixed task. cat full-instructions.txt | codex exec - → the file is the task. Pick the mode that matches which part is variable.
The flags you'll actually use
codex exec has a focused set of flags built for scripting. Here are the ones that earn their keep, all verified against the CLI reference:
| Flag | Alias | What it does |
|---|---|---|
--json | --experimental-json | Emit a newline-delimited JSON (JSONL) event stream on stdout instead of formatted text — the machine-readable contract |
--output-last-message <path> | -o | Also write the final assistant message to a file (it still prints to stdout too) |
--output-schema <path> | Force the final response to conform to a JSON Schema file | |
--sandbox <policy> | -s | read-only (default) | workspace-write | danger-full-access |
--model <name> | -m | Override the configured model for this run |
--skip-git-repo-check | Allow running outside a Git repository (the CLI otherwise expects one) | |
--ephemeral | Do not persist the session rollout file to disk | |
--color <when> | always | never | auto — pin or disable ANSI color (handy for clean CI logs) | |
--cd <path> | -C | Change working directory before executing |
Global flags also apply to exec, including --ask-for-approval / -a (untrusted | on-request | never) for approval policy, --config / -c key=value (repeatable, overrides any config key), --image / -i, --profile / -p, and --search.
# Pin the model, run outside a git repo, write the answer to a file, no color
codex exec \
--model gpt-5.5 \
--skip-git-repo-check \
--output-last-message answer.md \
--color never \
"draft release notes from the CHANGELOG"Two of these flags — --json and --output-schema — are the gateway to programmable Codex: parsing exactly what it did, and forcing schema-conforming output you can validate instead of free text. They're the entire subject of the next lesson, so this one just plants the flag; don't worry about the event shape yet.
A note on volatile values: exact model identifiers (--model gpt-5.5, gpt-5.4-codex, and whatever is "recommended" today) shift release to release, and the CLI/SDK versions move together. Don't hard-code a model name from memory — run codex exec --help for the live flag list and check the changelog for current model ids.
Tip
--skip-git-repo-check for non-repo automation
By default the CLI expects to run inside a Git repository. For tasks that operate on a loose directory — a docs folder, a scratch workspace, a generated artifact — add --skip-git-repo-check so exec doesn't refuse to start.
Watch out
Model names and versions rot
Model ids like gpt-5.5 / gpt-5.4-codex and the default "recommended" model change frequently, and the CLI and SDK versions are pinned together. Verify the current model with codex exec --help and the live changelog (developers.openai.com/codex/changelog) rather than trusting a value baked into a script or a tutorial.
Resuming a headless session
A headless run doesn't have to be a dead end. Codex persists each session so a later invocation can pick up the same thread — its conversation history and context — and continue. That turns a one-shot into a resumable, multi-step automation: a first job investigates, a second (perhaps in a different workflow step) acts on what the first learned.
# Resume a specific session by id and add a follow-up instruction
codex exec resume <SESSION_ID> "now implement the fix you proposed"
# Resume the most recent session from THIS directory
codex exec resume --last "continue where you left off"
# The follow-up prompt is optional — resume can simply re-run the thread
codex exec resume --lastTwo modifiers shape what "resume" can find:
| Option | Effect |
|---|---|
--last | Resume the most recent session from the current working directory |
--all | Consider sessions from any directory, not just the current cwd |
Where do these sessions live? Under ~/.codex/sessions by default — unless you ran the original with --ephemeral, which tells Codex not to persist the rollout file at all. That's the trade-off to internalize: --ephemeral is the privacy/cleanliness choice for sensitive or throwaway runs, but it means there is nothing to resume. If your pipeline needs a second step to continue the first, don't make the first one ephemeral.
To make this concrete, a two-phase pattern: a first read-only pass diagnoses a problem and prints a session id you capture (you'll learn to pull that id cleanly from the --json stream's thread.started event next lesson); a second pass resumes that session with workspace-write to actually apply the fix.
Watch out
--ephemeral means nothing to resume
Sessions persist under ~/.codex/sessions so codex exec resume can pick them up. Pass --ephemeral and the rollout file is never written — great for sensitive one-shots, but there is then no session for a later step to continue. Choose ephemerality per the task's needs.
Try it: Build a read-only-then-write headless pipeline
Goal: feel the full codex exec model — the stream split, the sandbox default, stdin modes, and resume — in one short pipeline. 1) Confirm auth. Make sure Codex is authenticated (ChatGPT login or OPENAI_API_KEY); a headless run can't prompt you to sign in. 2) Capture just the answer. In a small repo, run RESULT="$(codex exec 'in one sentence, what does this project do?')" then echo "$RESULT". Notice the progress scrolled by on stderr but $RESULT holds only the final message. 3) Prove the read-only default. Run codex exec 'add a comment to the top of README.md' and confirm the file is unchanged — the default sandbox is read-only. 4) Opt into edits. Rerun with codex exec --sandbox workspace-write 'add a one-line comment to the top of README.md' and confirm the edit landed. 5) Pipe data in both ways. Try Mode 1: npm test 2>&1 | codex exec "summarize any failures in 3 bullets" (the log is context). Then Mode 2: printf 'List the public exports of %s\n' src/index.ts | codex exec - (stdin is the whole prompt). 6) Resume. Note the session id from a non-ephemeral run, then continue it with codex exec resume --last "now propose the smallest fix". 7) Reflect (3 sentences): where did the stderr/stdout split save you from log-scraping, why did step 3 make no changes, and when would you choose --ephemeral and give up the ability to resume? This builds the instinct every later automation lesson assumes — that codex exec is a well-behaved Unix command with a deliberate safety default.
Key takeaways
- 1`codex exec` (alias `codex e`) runs a single task non-interactively and exits — it's the foundation of all Codex automation (CI, cron, Git hooks, shell pipelines), versus interactive `codex`, which opens a REPL.
- 2Progress streams to stderr and only the final agent message goes to stdout (or a JSONL stream with `--json`), so `RESULT="$(codex exec '...')"` cleanly captures just the answer; treat a non-zero exit code as failure.
- 3The exec sandbox defaults to `read-only`; you must opt into `--sandbox workspace-write` to edit files, and reserve `danger-full-access` / `--yolo` for trusted, ephemeral environments — sandbox (WHERE) and approvals (WHEN) are independent controls.
- 4There are two stdin modes: prompt-plus-stdin (`npm test 2>&1 | codex exec "summarize"`) treats piped data as context for a fixed task, while `codex exec -` makes stdin the entire prompt.
- 5Core flags: `--json`/`--experimental-json`, `--output-last-message`/`-o`, `--output-schema`, `--sandbox`/`-s`, `--model`/`-m`, `--skip-git-repo-check`, `--ephemeral`, `--color` — and model ids/versions are volatile, so verify them live rather than hard-coding.
- 6`codex exec resume <SESSION_ID>` (with `--last` / `--all`) continues a prior thread; sessions persist under `~/.codex/sessions` unless the original run used `--ephemeral`, in which case there is nothing to resume.
Quiz
Lock in what you learned
Check your understanding
0 / 4 answered
1.You're writing a CI step that needs to run a Codex task with no human present, capture its final answer into a shell variable, and continue the pipeline. Why is `codex exec` the right tool, and how do you capture just the answer?
2.A teammate runs `codex exec "refactor the database layer and update the imports"` in a script and is surprised that no files changed. What happened, and what's the fix?
3.What is the difference between `cat error.log | codex exec "explain the root cause"` and `cat error.log | codex exec -`?
4.A pipeline runs a first `codex exec` pass to diagnose an issue, then wants a second pass to `codex exec resume --last` and apply the fix. The resume fails to find any session. What's the most likely cause?
Go deeper
Hand-picked sources to keep learning
The canonical reference for headless mode: the stdout/stderr split, the two stdin modes, --json, --output-schema, and resuming sessions.
Full flag list for codex and codex exec, including --sandbox, --model, --skip-git-repo-check, --ephemeral, and the global approval/config flags.
Check here for the current default/recommended model and exact model identifiers — these are volatile and move with each release.
How to authenticate headless runs — ChatGPT account login vs OPENAI_API_KEY, plus the exec-scoped CODEX_API_KEY variable.
The open-source Rust CLI: source, release tags, and changelog. Verify your installed CLI version and its coupled SDK version here.