Where Codex stores session history
Codex writes one JSONL file per session, filed by the date it started:
~/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-<timestamp>-<uuid>.jsonlA real one looks like this:
~/.codex/sessions/2026/03/20/rollout-2026-03-20T09-55-16-019d08f4-37fe-7090-96a6-100d7fbe09df.jsonlArchived sessions move to a sibling directory:
~/.codex/archived_sessions/Codex also keeps a schema-versioned state database at ~/.codex/state_<N>.sqlite, where <N> climbs with each schema change. The highest-numbered file is the live one.
The path does not tell you the project
The directory hierarchy is dates, not projects. The working directory is recorded in session_meta.payload.cwd inside the file. So "show me every Codex session for this repo" cannot be answered from a directory listing. Something has to open each file and read the cwd out of it.
Two kinds of record
Codex's JSONL is closer to an event stream than a message log, and it carries each turn twice at different levels of abstraction:
| Record | What it is |
|---|---|
event_msg | The high-level conversation event. Clean text, already typed |
response_item | The raw model API item: tool calls, multi-part content arrays |
Use event_msg for readable text, and response_item when you need tool-call detail or attachments.
Images arrive split across both
A single user turn that contained a pasted image is written as two records. The response_item with role: user carries the input_image block with the actual image data, and the event_msg.user_message carries the clean typed text. Neither is complete on its own. To render the bubble the way it happened, you have to buffer the image and attach it to the matching text event.
Reading a Codex session from the terminal
Print the conversation text:
jq -r 'select(.type=="event_msg")
| .payload // . | select(.type=="user_message" or .type=="agent_message")
| .message // .text' \
~/.codex/sessions/2026/03/20/rollout-*.jsonlFind which project a session belongs to, without opening it in an editor:
jq -r 'select(.type=="session_meta") | .payload.cwd // empty' SESSION.jsonl | head -1Group every session by project. The directory layout will not give you this listing, so the loop has to open every file:
for f in ~/.codex/sessions/*/*/*/rollout-*.jsonl; do
jq -r 'select(.type=="session_meta") | .payload.cwd // empty' "$f" 2>/dev/null | head -1
done | sort | uniq -c | sort -rnHow do I resume this session?
Run codex resume SESSION_ID from the project's directory with Codex installed, or use the app's terminal or in-app chat. The app excludes archived sessions from the ordinary list; open its archived-session view to find them.
Source and limitations
Implementation: Codex adapter. Upstream: Codex documentation.
Reference scope: checked against the Sessions Viewer 0.6.0 adapters on 2026-10-05, not a compatibility guarantee for every CLI release. Shell examples use macOS/Linux paths and require
jq(orsqlite3for opencode). Replace placeholders with real values and use your configured data root. Text extraction does not replay tools, images or the active branch of a session tree. Command regressions use downloadable synthetic samples, not private sessions. Check the capability and evidence matrix for tested scope. Browsing is distinct from resuming or editing data; see privacy and data handling.
The viewer reads the home-directory layout above; a CLI writing to a different CODEX_HOME is not automatically covered. See missing-session troubleshooting.
Or open it in an app
Follow the Codex project, archive, export and resume workflow for step-by-step use and manual-vs-GUI tradeoffs.
Sessions Viewer does that grouping for you and caches it, pairs split image records back together, and can show subscription quota windows when the authenticated account exposes them. Quota is not a token-cost invoice; see statistics. It also reads Claude Code, Grok Build, Kimi Code, Pi, Antigravity CLI and opencode.
