I build ccdeck, a local dashboard that reads these files, and it is the example in this post; every switch is in its Codex guide. The file samples are from codex-cli 0.162.1 on my machine, Codex's docs are quoted as I read them on 14 October 2026, and ccdeck is described as of 3.48.0.
I run Codex CLI in two or three repositories at once, each in its own terminal, each a scroll of commands, patches and output. Codex has part of the overview itself, and it is worth starting there:
-
codex agents, added in Codex CLI 0.149.0, is "an interactivecodex agentsdashboard for searching, starting, opening, renaming, and stopping tasks".codex queue, from the same release, sends messages to existing sessions. - Inside a session,
/statusshows its model, approval policy, writable roots and current token usage, and/statuslinecan keep rate limits and token counters in Codex's footer.
For anything else, such as every session's tool calls as they happen, or a view that does not depend on which terminal you are in, the source is a file Codex already writes.
Where the files are
Codex's configuration docs say it "stores its local state under CODEX_HOME (defaults to ~/.codex)", and name config.toml, auth.json, logs and history.jsonl there. They do not describe the per-session files, which sit beside them:
~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl
One file per session, in the folder for the day it started. Because the docs leave them out, treat the format as Codex's own: read it defensively, and expect new kinds of line.
What a line looks like
Each line is one JSON object with a timestamp, a type and a payload. Counting the kinds in one of my sessions:
f=$(ls -t ~/.codex/sessions/*/*/*/rollout-*.jsonl | head -1)
jq -r '[.type, (.payload.type // "-")] | join(" ")' "$f" | sort | uniq -c | sort -rn
468 event_msg item_completed
178 event_msg token_count
176 token_usage_record -
134 response_item custom_tool_call_output
134 response_item custom_tool_call
115 response_item reasoning
97 response_item message
28 turn_context -
27 event_msg task_started
27 event_msg task_complete
13 response_item function_call_output
13 response_item function_call
1 session_meta -
(trimmed). The ones a live view needs:
-
session_meta, once at the top: the session id,cwd,cli_version,model_provider. -
turn_context, once per turn:model,approval_policy,sandbox_policy,effort. Join mid-session and you know neither the model nor whether the session can stop to ask until the next one arrives. -
response_item: the conversation. A tool call is afunction_callorcustom_tool_callline, and its result the matching…_outputline with the samecall_id. -
event_msg: what happened.task_startedandtask_completebracket a turn;token_countcarries token usage and a rate-limit snapshot.
Following a session live
The file is appended to as the session runs, so following it is tail plus a parser:
tail -n 0 -F "$f" | jq --unbuffered -r '[.timestamp, .type, (.payload.type // "")] | @tsv'
A program that does this for every session has three more things to handle:
-
New files. A new session is a new file in today's folder, and a long session, or one picked up with
codex resume, keeps writing to its file in an older day folder. -
Joining late. Start reading mid-session and you have no
turn_contextuntil the next turn: no model, no approval policy, no price for the tokens. - Partial lines. The last line can be half-written when you read it; keep it until its newline arrives.
ccdeck is one such program. It checks for new lines every 1.5 seconds and turns each session into a card named after its folder:

Demo data. The last row counts the calls, then gives the tokens and an estimated cost.
The pill reads LIVE while a turn runs and DONE when Codex finishes it, or when you press Esc in Codex. Tool calls get short names: Shell for exec/shell and friends, Edit for apply_patch, Plan for update_plan, Web, stdin and wait. A session that was already running when the deck started appears the next time it writes, from that point on, with ? beside its name, ≥ before its time and a plain Codex chip until its next turn starts: point 2 above, made visible. One caught mid-turn, its file written in the last 24 hours, is read back from the end of its file instead and appears at once, marked restored.
Failure is a guess. The file records a tool's output, not a status. ccdeck counts a call as failed only when the output begins "Script failed", or "Exit code:" with a number other than 0, so a command that failed in some other way can still show ✓. Read the output before you trust the tick.
Tokens, cost and quota from the same file
token_count events hold two things.
Usage. An info object with total_token_usage, last_token_usage and model_context_window: input (cached tokens included), output, and the context-window figures from Codex's most recent request. Pricing them needs a rate, and here the file goes only part of the way: it can say which service tier a session asked for (ccdeck reads it from a thread_settings_applied event), not the tier a request was billed at. ccdeck prices cached tokens at the cache rate, the rest at the tier recorded for them (the standard rate where none is), Codex's auto-review checks on a ChatGPT plan at $0, and labels the result an estimate.
A rate-limit snapshot. A rate_limits object with primary and secondary windows, each holding used_percent, window_minutes and resets_at:
jq -c 'select(.type == "event_msg" and .payload.type == "token_count")
| .payload.rate_limits.primary' "$f" | tail -1
{"used_percent":93.0,"window_minutes":10080,"resets_at":1792383934}
On my account "primary" was the 7-day window, so name windows by window_minutes, not by position. And it is a snapshot from the last request: a session that has been idle for an hour holds an hour-old reading. ccdeck reads the quota about once a minute while its page is open, from Codex's usage endpoint rather than the file, and names each bar by its length (5-hour window, 7-day window, 30-day window, or Rate limit when OpenAI gives no length).
What the file never says
When a session is waiting for your approval. Codex writes no approval request to the rollout file. A call waiting for you looks exactly like one still running. ccdeck's answer is to say so rather than guess: a running card shows approvals not visible whenever its approval_policy can ask (untrusted, on-request, on-failure, or not read yet), and nothing at never.
Codex itself has two ways to tell you, both in its config: tui.notifications "can optionally filter by event type (for example, agent-turn-complete and approval-requested)", and notify runs a program of yours, though only for agent-turn-complete, with a JSON argument. Codex's app-server also reports a waiting status; reading that from ccdeck is open work.
When a session closed. Nothing is written when you close the terminal. ccdeck keeps such a card LIVE until 90 minutes pass with no new line.
ccdeck also draws no title for a Codex session, which a Claude Code card has, and joins a Codex sub-agent to its parent only when the file names that parent (parent_thread_id in its session_meta, or a sub-agent started event in the parent's file).
The one write: refreshing the token safely
Reading Codex's quota needs a live token from $CODEX_HOME/auth.json, and tokens expire. Refreshing one from outside the CLI is where a reader can do damage: OpenAI's refresh tokens are single-use, so a rotation that never reaches disk leaves auth.json holding a token that no longer works, and the next step is codex login.
ccdeck's refresh is a pattern worth copying for any file another program also writes:
- Only when needed: the token is within 90 seconds of expiring, or the usage endpoint turned it down.
-
One refresh at a time, and the file is re-read inside a lock, in case
codexrefreshed it a moment earlier. - The write is atomic, and only the token fields are written; nothing else in
auth.jsonis touched. - It happens only while a deck page is open or a quota notification is on.
The quota needs Codex signed in with codex login and a ChatGPT account. With an API key there is no quota to read.
Why read files instead of installing a hook
The rollout file is on every system Codex runs on, and reading it installs nothing and asks for no trust prompt; starting ccdeck writes nothing in the Codex folder (the token refresh above is the one write, and only when needed). The cost of following rollout files is described above: a file written for Codex's own history tells you what happened, not what Codex is waiting on.
If you try this yourself, the short version: ls -t for the newest file, tail -F it, key off type and payload.type, take the model and policy from turn_context, and name quota windows by their minutes.
AI assistance was used to prepare this article and check it against the cited source code and documentation.

Top comments (0)