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.
codex exec --helpRun 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 repositoryOne 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:
codex exec "generate release notes for the last 10 commits" | tee release-notes.mdIt will not run just anywhere
A real run, in a folder that is not a git repository:
codex exec "say hi"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:
{"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.
"""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.
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.
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
| Flag | What it does |
|---|---|
-o <path> | Write only the final message to a file |
--output-schema | Require the final answer to match a JSON Schema |
--ephemeral | Do 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.
- Run
codex execwith--jsonand pipe it throughjq. - Change the usage numbers in the panel and rerun the parser.
This is what real progress feels like.