OpenAI Codexcodex-cli 0.154 · macOS, Linux, WSL, Windows
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
26 small wins to finish your pathNext lesson

codex exec: one prompt, no session

Everything so far has been a conversation. codex exec makes it a command, and a command can go in a script, a pipeline or CI.

bash
codex exec --help
Captured from a real run
Run Codex non-interactively

Usage: codex exec [OPTIONS] [PROMPT]
       codex exec [OPTIONS] <COMMAND> [ARGS]

Commands:
  resume  Resume a previous session by id or pick the most recent with --last
  fork    Fork a previous session by id into a new session
  review  Run a code review against the current repository

One prompt in, the final answer out. While it works, progress goes to standard error and only the final message goes to standard output, which is what makes the next line work:

bash
codex exec "generate release notes for the last 10 commits" | tee release-notes.md

It will not run just anywhere

A real run, in a folder that is not a git repository:

bash
codex exec "say hi"
Captured from a real run
Not inside a trusted directory and --skip-git-repo-check was not specified.

That is deliberate. An agent that edits files should be somewhere you can see what changed and undo it, and git is how you do that. The flag exists for the cases where you mean it.

Structured output

--json turns standard output into JSON Lines, one object per event. This is the shape, from the documentation:

json
{"type":"thread.started","thread_id":"0199a213-81c0-7800-8aa1-bbab2a035a53"}
{"type":"turn.started"}
{"type":"item.started","item":{"id":"item_1","type":"command_execution","command":"bash -lc ls","status":"in_progress"}}
{"type":"item.completed","item":{"id":"item_3","type":"agent_message","text":"Repo contains docs, sdk, and examples directories."}}
{"type":"turn.completed","usage":{"input_tokens":24763,"cached_input_tokens":24448,"output_tokens":122,"reasoning_output_tokens":0}}

Threads start, turns start, items start and complete, and the turn completes with the token usage. Everything a script needs is in there.

Reading it

The panel beside this reads that stream and pulls out the three things a pipeline actually wants. Press Run.

Example
"""Read a codex exec run the way a script would.

With --json, stdout is one JSON object per line.
"""
import json

events = [json.loads(line) for line in open("run.jsonl")]

print("events:", len(events))
print("kinds: ", sorted({e["type"] for e in events}))

Five events for one short run. Every line is one of those five kinds, which is the whole format: a thread starts, a turn starts, items start and complete, the turn completes.

Example
for event in events:
    item = event.get("item", {})
    if event["type"] == "item.started" and item.get("type") == "command_execution":
        print("ran: ", item["command"])
    if event["type"] == "item.completed" and item.get("type") == "agent_message":
        print("said:", item["text"])

The items are what happened. A command execution says what it ran, and an agent message is what it said at the end, which is the part a pipeline usually wants.

Example
used = next(e for e in events if e["type"] == "turn.completed")["usage"]

print("tokens in: ", used["input_tokens"], "of which cached", used["cached_input_tokens"])
print("tokens out:", used["output_tokens"])

Commands it ran, what it said at the end, and what the turn cost. The cached input count is worth watching in CI: a job that reruns on every push pays far less when the prefix is cached.

Three flags worth remembering

FlagWhat it does
-o <path>Write only the final message to a file
--output-schemaRequire the final answer to match a JSON Schema
--ephemeralDo not keep session files on disk

--output-schema is the one that turns Codex into something the next step of a pipeline can rely on: the fields are fixed, so the script after it does not have to parse English.

Unattended means explicit
Whatever you run unattended runs under the sandbox and approval settings you set, and nobody is there to answer a prompt. Set both explicitly in the command rather than inheriting whatever your config happens to say.
Try it yourself
  • Run codex exec with --json and pipe it through jq.
  • Change the usage numbers in the panel and rerun the parser.

This is what real progress feels like.