skip to content

codex exec — Non-Interactive Mode

Complete reference for codex exec — scripted invocation, stdin piping, JSON event streams, output formats, exit codes, timeouts, and automation patterns for CI, git hooks, and shell pipelines.

13 min read 84 snippets deep dive

codex exec — Non-Interactive Mode#

What it is#

codex exec is Codex CLI’s headless one-shot mode. It accepts a prompt (as an argument, from stdin, or both), runs the agent loop to completion, streams output to stdout, and exits with a status code reflecting success or failure. It does NOT open the TUI, does NOT prompt the user unless --ask-for-approval is enabled, and does NOT consume the interactive history file unless --save is passed. It is the primary surface for CI pipelines, git hooks, editor save-actions, shell aliases, and anything else that needs a deterministic exit.


Invocation syntax#

codex exec [OPTIONS] [PROMPT]

Output: (none — syntax reference only)

The PROMPT argument is optional — if omitted, Codex reads the prompt from stdin. If both are present, stdin content is appended to the prompt as additional context.

codex exec "What does this repo do?"

Output:

This repository is a personal cheat-sheet site built with Astro 4 and Tailwind CSS. …

Reading prompts from stdin#

The most useful exec pattern: pipe a file, a diff, a log, or any other long context into Codex without ballooning the command line.

git diff main...HEAD | codex exec --output-last-message "Summarise this diff for a PR description"

Output:

This PR refactors the authentication module to use JWT tokens, adds rate limiting middleware, and updates the test suite to cover token rotation.

Pipe a file with an explicit prompt:

cat src/auth.py | codex exec "Review this file for security issues; output bullets only"

Output:

- Token compared with == instead of secrets.compare_digest (line 42)
- SQL query constructed via f-string, not parameterised (line 78)
- No rate limit on login endpoint

Empty prompt argument, full stdin:

echo "Write a haiku about TOML" | codex exec

Output:

Tables nest like pines
Keys whisper to silent vals
Brackets hold the dawn

Output formats#

The --output-format flag controls how Codex streams the agent’s progress and final answer. Choose text for human reading, last-message for shell pipelines, and json / stream-json for programmatic consumers.

FormatFlagDescription
Text--output-format text (default)Streamed plain text with diff previews
Last message--output-last-messageOnly the final assistant message, plain text
JSON--output-format jsonSingle JSON object printed after completion
Stream JSON--output-format stream-jsonNewline-delimited JSON events as they arrive
NDJSON events--json (alias for stream-json)Same as stream-json

Text output#

The default — useful when a human is watching the terminal.

codex exec "Add a docstring to every public function in src/utils.py"

Output:

[agent] Reading src/utils.py…
[agent] Adding docstrings to 4 functions.
[diff] src/utils.py: +24 lines
[agent] Done. 4 docstrings added.

Last-message output#

Strips all intermediate output and prints only the final assistant message. Ideal for $(...) capture.

title=$(codex exec --output-last-message "Suggest a 6-word PR title for the staged diff" < <(git diff --cached))
echo "$title"

Output:

Refactor auth module to use JWT

JSON output#

A single JSON object with full session metadata, printed atomically after the agent finishes.

codex exec --output-format json "List three Linux process-inspection tools as JSON"

Output:

{"type":"result","subtype":"success","session_id":"th_01abc","turn_count":1,"last_message":"[\"ps\",\"top\",\"lsof\"]","exit_code":0}

Parse with jq:

codex exec --output-format json "Run pytest and report the count" | jq -r '.last_message'

Output:

142 tests passed, 0 failed.

Stream-JSON / NDJSON#

One JSON object per line, emitted as events occur. The richest format for programmatic consumers — you see every tool call, every result, and the final turn-complete event.

codex exec --json "Run the test suite and summarise failures"

Output:

{"type":"session-start","session_id":"th_01abc","model":"gpt-5","cwd":"/home/alice/myproject"}
{"type":"agent-turn-start","turn_id":"t_01"}
{"type":"tool-call","tool":"shell","input":{"cmd":"pytest"}}
{"type":"tool-result","tool":"shell","output":"142 passed, 3 failed"}
{"type":"agent-turn-complete","turn_id":"t_01","last_message":"3 tests failed: test_auth, test_db_pool, test_jwt_refresh."}
{"type":"session-end","session_id":"th_01abc","exit_code":0}

Filter to just final messages with jq:

codex exec --json "What does this repo do?" | jq -r 'select(.type=="agent-turn-complete") | .last_message'

Output:

This repository is a personal cheat-sheet site …

Exit codes#

codex exec returns a meaningful exit code that you can use in shell pipelines. The most common codes:

CodeMeaning
0Success — agent completed and produced a final message
1Generic error — model error, network error, or unrecognised failure
2Approval denied — user (or hook) refused a required tool call
3Sandbox violation — agent attempted a denied operation and could not recover
4Authentication error — invalid or missing OPENAI_API_KEY / expired token
5Configuration error — config.toml failed to parse
6Timeout — --timeout exceeded
7Max-turns exceeded — --max-turns N reached before the agent finished
130Cancelled — user pressed Ctrl+C (SIGINT)

Use exit codes in pipelines:

codex exec --full-auto "Fix all ruff issues" || echo "Codex run failed with code $?" >&2

Output:

[agent fixes issues; exits 0 on success]

Fail-fast in a chain:

codex exec -p ci "Bump version in pyproject.toml" \
  && codex exec -p ci "Write CHANGELOG entry for the bump" \
  && git commit -am "chore: bump version"

Output:

[two agent runs followed by a commit; aborts on first failure]

Common flags#

The flags most often needed in exec mode. The full list also includes everything from interactive mode (model, profile, sandbox, etc.).

FlagDescription
--output-format <fmt>text / json / stream-json
--output-last-messageEquivalent to --output-format last-message
--jsonAlias for --output-format stream-json
--max-turns <n>Cap agent loop at N turns (default unlimited)
--timeout <secs>Wall-clock timeout for the whole run
--ephemeralDon’t write the session to history
--savePersist the session even though it’s non-interactive
--skip-git-repo-checkAllow running outside a git repo
--full-autoShorthand for --sandbox workspace-write --ask-for-approval on-request
--ask-for-approval <policy>Override approval policy (default never in exec)
--sandbox <mode>Override sandbox mode
--profile <name>Activate a named profile
--cd <dir>Run as if cwd were <dir>

Timeouts and turn caps#

In exec mode the agent can loop indefinitely if no terminating condition is reached. Two guard-rails prevent runaway runs: wall-clock timeout and max-turns.

Wall-clock timeout#

codex exec --timeout 120 "Fix any failing tests"

Output:

[agent runs for up to 120 seconds; exits with code 6 if not done in time]

Max-turns cap#

codex exec --max-turns 8 "Fix any failing tests"

Output:

[agent runs for at most 8 tool-use cycles; exits with code 7 if it hits the cap]

Combine both#

codex exec --timeout 300 --max-turns 20 "Refactor the auth module"

Output:

[bounded by both wall-clock and turn count, whichever trips first]

Ephemeral vs. persisted sessions#

By default, codex exec does NOT save to ~/.codex/history/ — it’s “fire and forget.” Pass --save if you want to resume the session later. Pass --ephemeral to be explicit about throwaway use.

codex exec --save "Refactor the DB layer"

Output:

[agent output]
Saved session as th_01xyz.
codex resume th_01xyz

Output: (TUI opens at that session’s end state)


Working directory and git checks#

codex exec refuses to run outside a git repository by default — a guard against accidental edits to a random directory. Override with --skip-git-repo-check:

mkdir /tmp/scratch && cd /tmp/scratch
codex exec --skip-git-repo-check "Create a hello-world Flask app"

Output:

[agent creates app.py, requirements.txt, README.md]

Run in a different directory without cd:

codex exec --cd /home/alice/work/api "Run the test suite"

Output:

[agent runs pytest in /home/alice/work/api]

Combining with --full-auto#

--full-auto plus codex exec is the canonical “hands-free” pattern: workspace-confined writes, no network unless explicitly allowed, no interactive prompts (since exec defaults to --ask-for-approval never).

codex exec --full-auto "Fix all ruff lint errors in src/"

Output:

[agent reads files, runs ruff --fix, edits remaining issues, exits]

Per-profile shorthand:

[profiles.ci]
model              = "gpt-4o-mini"
approval_policy    = "never"
sandbox_mode       = "workspace-write"
codex exec -p ci "Fix all type errors in src/"

Output:

[agent output; exits 0 on success]

Programmatic consumers — parsing stream-json#

The richest output format for tools that need to react to events in real time. Each line is a self-contained JSON object whose type field tells you what it is.

Event types#

TypePayload
session-startsession_id, model, cwd, profile
agent-turn-startturn_id
tool-calltool, input (tool-specific)
tool-resulttool, output, success
agent-message-deltadelta (streaming text chunk)
agent-turn-completeturn_id, last_message, tokens
session-endsession_id, exit_code, total_tokens
errorcode, message

Stream tokens as they arrive#

codex exec --json "Write a haiku" | jq -j 'select(.type=="agent-message-delta") | .delta'
echo

Output:

Lines write themselves
Syllables fall into place
Codex paints the page

Count tool calls by type#

codex exec --json "Refactor the auth module" \
  | jq -r 'select(.type=="tool-call") | .tool' \
  | sort | uniq -c

Output:

  12 shell
   8 read_file
   4 write_file

React to errors#

codex exec --json "Risky task" \
  | tee codex-stream.jsonl \
  | jq -rc 'select(.type=="error") | "ERROR: " + .message' >&2

Output (stderr):

ERROR: sandbox denied write to /etc/hosts

Combining exec with shell pipelines#

codex exec is designed to be one node in a Unix pipeline. Read from stdin, write to stdout, exit with a code — that’s it.

Filter through Codex#

git log --since=yesterday --oneline \
  | codex exec --output-last-message "Group these commits by area" \
  | tee yesterday-summary.txt

Output:

Auth: 3 commits — JWT migration, rate limit, refresh fix
DB:   2 commits — pool config, retry helper
Docs: 1 commit  — README update

Two-stage pipeline#

git diff main...HEAD \
  | codex exec --output-last-message "Extract a one-line summary of each functional change" \
  | codex exec --output-last-message "Format as a markdown bullet list"

Output:

- Replaced session cookies with JWT
- Added per-IP rate limit middleware
- Tightened DB pool config

Fan-out one prompt to many files#

for f in src/**/*.py; do
  result=$(codex exec --output-last-message --skip-git-repo-check \
    "Suggest one improvement for this file" < "$f")
  printf '%-30s  %s\n' "$f" "$result"
done

Output:

src/auth.py                   Extract token validation into a helper
src/db.py                     Replace string SQL with parameterised
src/services.py               Add type hints to handle_request

CI integration patterns#

GitHub Actions — fail PR if Codex finds issues#

- name: AI review
  env:
    OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
  run: |
    git diff origin/main...HEAD \
      | codex exec --output-format json --sandbox read-only --ask-for-approval never \
          "Find correctness bugs in this diff. Reply with JSON array of objects {file, line, issue}." \
      | jq -e '.last_message | fromjson | length == 0'

Output (when issues found):

[{"file":"src/auth.py","line":42,"issue":"==-comparison of tokens"}]
Process completed with exit code 1.

Pre-commit hook — auto-fix and re-stage#

#!/usr/bin/env bash
# .git/hooks/pre-commit
codex exec --full-auto -p ci "Run ruff format and fix any mypy errors in the staged files."
git add -u

Output:

[agent fixes issues; commit proceeds with re-staged files]

Daily summary cron job#

# crontab entry
0 18 * * * cd ~/work/myproject && codex exec --sandbox read-only --ask-for-approval never \
  "Summarise today's commits and open PRs" | mail -s "Daily summary" alice@example.com

Output: (none — email delivered with summary in body)

Self-healing test fixer#

pytest -x || codex exec --full-auto "Fix the failing test you just saw"

Output:

[agent reads the pytest traceback, locates the bug, edits the file]

Reading and writing structured data#

Generate JSON from a description#

codex exec --output-last-message "Generate a JSON object with 3 fake users: id, name, email. JSON only, no prose." | jq .

Output:

[
  {"id": 1, "name": "Alice Dev", "email": "alice@example.com"},
  {"id": 2, "name": "Bob Eng",   "email": "bob@example.com"},
  {"id": 3, "name": "Cara Ops",  "email": "cara@example.com"}
]

Round-trip a YAML through Codex#

cat config.yaml \
  | codex exec --output-last-message "Convert this YAML to a flat .env file" \
  > .env.from-yaml

Output: (none — writes .env.from-yaml)


Common pitfalls#

  1. codex exec defaults to --ask-for-approval never. This is the safe-for-CI default but it’s the opposite of interactive mode. If you want a pipe-and-prompt behaviour, pass --ask-for-approval on-request explicitly — but be aware it will hang in CI without a tty.

  2. stdin piping has a hard limit (~1 MB by default). Massive diffs or log files truncate silently. Use --input-file <path> for larger inputs.

  3. --output-last-message swallows tool errors. A failed tool call still produces a final assistant message; you can’t distinguish “succeeded” from “tried and gave up” by output alone. Check the exit code, or use --json and inspect agent-turn-complete.

  4. $(codex exec …) in bash strips trailing newlines. If your downstream consumer needs them, write to a file with > /tmp/out instead.

  5. codex exec does NOT inherit your TUI’s /permissions state. Each invocation starts with the policy from config.toml and CLI flags. Don’t assume “I approved this command interactively, so exec won’t ask.”

  6. A pre-existing background process (e.g., npm run dev &) is killed when exec exits. Codex’s sandbox cleanup is aggressive. Use nohup + disown if you genuinely need a process to survive.

  7. --max-turns 1 doesn’t mean “single API call.” It means “one agent loop iteration” — which can still include tool calls. Use --max-turns 0 to disable tools entirely (model-only response).

  8. JSON output buffers until completion (with --output-format json). Use --json (stream-json) if you need to react during the run.

  9. exec runs from cwd, not the project root. If you launch from a subdirectory, the sandbox boundary follows. Combine with --cd to anchor to a known root.

  10. No tty means no progress indicators. Codex detects the non-interactive context and disables the spinner; if you see “frozen-looking” output, that’s just because there’s no animation.


Real-world recipes#

One-liner repo summary#

codex exec --sandbox read-only --ask-for-approval never --output-last-message \
  "Summarise this repo in 3 sentences"

Output:

This repo is a personal cheat-sheet site built with Astro 4 and Tailwind CSS. Content lives in Markdown under src/content/. Cloudflare Pages auto-deploys on every push to main.

Replace grep | xargs sed with grep | codex#

For mechanical transforms that are easier to describe than to encode:

rg -l "TODO: legacy" src/ | xargs -I{} codex exec --full-auto \
  "Replace the 'TODO: legacy' marker in {} with a real implementation"

Output:

[for each file, agent edits and reports]

Streaming SSE to a Slack thread#

codex exec --json "Long task" \
  | jq -r 'select(.type=="agent-turn-complete") | .last_message' \
  | xargs -I{} curl -sS -X POST -H 'Content-Type: application/json' \
      -d "{\"text\":\"{}\"}" "$SLACK_WEBHOOK_URL"

Output:

[Slack messages appear as turns complete]

Lock the model and profile via env#

export CODEX_MODEL=gpt-5
export CODEX_PROFILE=ci
codex exec "Fix all mypy errors"

Output:

[agent runs with the locked model + profile]

Capture full transcript to a file#

codex exec --json "Refactor auth" > /tmp/run-$(date +%s).jsonl

Output: (none — JSONL written)

Re-derive a human transcript later:

jq -r 'select(.type=="agent-turn-complete") | .last_message' /tmp/run-*.jsonl

Output:

[final messages from each turn]

Diff explainer for code reviews#

git show HEAD | codex exec --output-last-message \
  "Explain what this commit changed and why, in 4 bullets"

Output:

- Introduced JWT helpers in src/auth/jwt.py
- Replaced session-cookie lookups in middleware
- Added refresh-token rotation logic
- Updated tests/test_auth.py to cover the new flow

Translate a stack trace into a fix proposal#

pytest 2>&1 | codex exec --output-last-message \
  "Read this pytest failure and propose the smallest fix. Output diff only."

Output:

--- a/src/auth.py
+++ b/src/auth.py
@@ -38,3 +38,3 @@
-    if token == expected:
+    if secrets.compare_digest(token, expected):

Idempotent retry wrapper#

# Retry up to 3 times with exponential backoff
for i in 1 2 3; do
  codex exec --timeout 60 --full-auto "Fix the flaky test in tests/test_auth.py" && break
  sleep $((2**i))
done

Output:

[agent attempts until success or 3 tries elapsed]

Multi-file fan-out with GNU parallel#

ls src/*.py | parallel -j 4 'codex exec --full-auto --skip-git-repo-check "Add docstrings to {}"'

Output:

[4 agents run in parallel; each edits its own file]