Exploring Codex Sessions
Set CODEX_ROOT="${CODEX_HOME:-$HOME/.codex}" and CODEX_DB_ROOT="${CODEX_SQLITE_HOME:-$CODEX_ROOT}". Codex CLI stores sessions as JSONL rollouts under $CODEX_ROOT; rollouts are canonical and SQLite is rebuildable. Writers are mixed; persistent exec is the proven paginated-default path.
Storage locations
| Path | What it holds |
|---|---|
$CODEX_ROOT/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl |
Full session transcripts. Date dirs use local time |
$CODEX_ROOT/archived_sessions/rollout-*.jsonl |
Archived sessions (flat, no date dirs, via codex archive) |
$CODEX_DB_ROOT/state_5.sqlite |
threads table: metadata index (cwd, preview, first_user_message, title, name, git info, history_mode, archived flag). Treat as cache — rebuilt from rollouts |
$CODEX_DB_ROOT/thread_history_1.sqlite |
Rebuildable transcript-item projection; contains item JSON but is not canonical |
$CODEX_DB_ROOT/queue_1.sqlite |
Pending user submissions; writing to it changes queue state |
$CODEX_ROOT/history.jsonl |
User-typed prompts: {"session_id", "ts" (unix sec), "text"}. Check its mtime first — it can silently stop being appended (see pitfall) |
$CODEX_ROOT/session_index.jsonl |
Thread names: {"id", "thread_name", "updated_at"}, last entry wins |
$CODEX_ROOT/config.toml |
[history] persistence = "save-all"|"none" |
Sessions are never auto-deleted (codex archive / codex delete are manual). Rollouts may be zstd-compressed to .jsonl.zst; search them when zstdgrep or zstdcat is available. The SQLite root follows CODEX_SQLITE_HOME, then CODEX_HOME; do not assume CODEX_HOME relocates every store. session_index.jsonl is an append-only compatibility index; prefer SQLite where available.
Rollout schema (quick reference)
Every wrapped line is a rollout envelope, optionally carrying sequential ordinal in current paginated files (legacy readers accept no ordinal). The eleven source variants are documented in data-model.md:
session_meta— line 1:id/session_id,cwd,cli_version,source(cli,vscode,exec,mcp, subagent objects),git{branch, commit_hash, repository_url},history_mode,context_window,forked_from_idresponse_item— model-visible conversation:message(rolesuser/assistant/developer),function_call(+_output),custom_tool_call(+_output, e.g.apply_patch),tool_search_call/tool_search_output,agent_message(cross-agent delivery),reasoning(encrypted),web_search_callevent_msg— legacy UI eventsuser_message/agent_message; paginateditem_completedcarriesUserMessage/AgentMessageTurnItems. Exclude commentary.turn_context— per-turn snapshot:model,cwd,approval_policy,sandbox_policy,permission_profile,collaboration_modeworld_state— model-visible world snapshot ({full, state}: skills, environments, permissions); not conversation — skip when building transcriptscompacted— compaction marker;replacement_history[]substitutes prior history on replayinter_agent_communication/_metadata— multi-agent only
Injected-context pitfall: response_item user and developer messages carry harness injections. In legacy format prefer event_msg/user_message; in paginated format prefer item_completed/UserMessage. Do not treat bare-era files as covered by these envelope recipes.
Format and integrity pitfall: current paginated files use item_completed; legacy files use user_message/agent_message. Desktop alpha corpora include malformed split records: raw jq can stop or truncate. Preflight before export and use the current projection/migration or an explicitly documented tolerant repair; do not silently hide failures with fromjson?.
Recipes
List recent sessions (fast path, via the index)
CODEX_ROOT="${CODEX_HOME:-$HOME/.codex}"
CODEX_DB_ROOT="${CODEX_SQLITE_HOME:-$CODEX_ROOT}"
sqlite3 -separator ' | ' "$CODEX_DB_ROOT/state_5.sqlite" \
"SELECT datetime(updated_at,'unixepoch','localtime'), substr(id,1,13), cwd,
substr(replace(first_user_message,char(10),' '),1,60)
FROM threads WHERE archived=0 ORDER BY updated_at DESC LIMIT 20;"
Use 13 id chars, not 8 — UUIDv7 prefixes collide for sessions started in the same instant. Add AND thread_source='user' to drop subagent/guardian threads, whose first_user_message is an injected block rather than human text.
List recent sessions (filesystem only — works on every version)
CODEX_ROOT="${CODEX_HOME:-$HOME/.codex}"
find "$CODEX_ROOT/sessions" "$CODEX_ROOT/archived_sessions" \( -name 'rollout-*.jsonl' -o -name 'rollout-*.jsonl.zst' \) -print 2>/dev/null |
while IFS= read -r f; do printf '%s\t%s\n' "${f##*/}" "$f"; done |
sort | tail -20 | cut -f2-
# Wrapped legacy cwd/prompt probes (use the dump recipe for paginated files; see the era doc for bare files):
# For a compressed FILE, pipe `zstdcat FILE` into the jq probes instead of reading it directly.
head -1 FILE | jq -r '.payload.cwd // .cwd // "?"'
jq -r 'select(.type=="event_msg" and .payload.type=="user_message") | .payload.message' FILE | head -1
Search all sessions for a keyword
CODEX_ROOT="${CODEX_HOME:-$HOME/.codex}"
rg -l --glob 'rollout-*.jsonl' 'KEYWORD' "$CODEX_ROOT/sessions" "$CODEX_ROOT/archived_sessions"
# or search only what the user typed, with session IDs — but check freshness first:
ls -l "$CODEX_ROOT/history.jsonl"
jq -r 'select(.text|test("KEYWORD";"i")) | "\(.session_id) \(.ts|todate) \(.text[0:80])"' "$CODEX_ROOT/history.jsonl"
This covers uncompressed active and archived files. For .zst rollouts, require zstd support and use zstdgrep or zstdcat; do not silently omit them.
history.jsonl can lag badly: on the verification host its last append was 2026-07-10 despite persistence = "save-all" and hundreds of later sessions, and codex exec never appends to it. If its mtime is older than the newest rollout, use the rg line or state_5.threads.first_user_message instead.
Resolve a logical session to its current rollout
CODEX_ROOT="${CODEX_HOME:-$HOME/.codex}"
CODEX_DB_ROOT="${CODEX_SQLITE_HOME:-$CODEX_ROOT}"
case "$ID" in (*[!0-9a-fA-F-]*|'') echo 'Invalid ID prefix' >&2; exit 2;; esac
sqlite3 -separator ' | ' "$CODEX_DB_ROOT/state_5.sqlite" \
"SELECT id, rollout_path FROM threads WHERE id LIKE '${ID}%';"
Dump a transcript as markdown
jq empty FILE >/dev/null || {
echo 'Malformed rollout: use the current projection/migration or explicitly repair it first.' >&2; exit 1;
}
jq -r '
if .type=="event_msg" then .payload |
if .type=="user_message" then "## User\n\n\(.message)\n"
elif .type=="agent_message" and ((.phase//"final")!="commentary") then "## Codex\n\n\(.message)\n"
elif .type=="item_completed" and .item.type=="UserMessage" then "## User\n\n" + ([.item.content[]? | select(.type=="text") | .text] | join("\n")) + "\n"
elif .type=="item_completed" and .item.type=="AgentMessage" and ((.item.phase//"final")!="commentary") then "## Codex\n\n" + ([.item.content[]? | select(.type=="Text") | .text] | join("\n")) + "\n"
elif .type=="item_completed" and (.item.type=="CollabAgentToolCall" or .item.type=="CommandExecution" or .item.type=="DynamicToolCall" or .item.type=="FileChange" or .item.type=="ImageView" or .item.type=="McpToolCall" or .item.type=="WebSearch") then " [tool] \(.item.type)\n"
else empty end
elif .type=="response_item" and (.payload.type=="function_call" or .payload.type=="custom_tool_call" or .payload.type=="tool_search_call") then
" [tool] \(.payload.name // .payload.type)\n"
else empty end' FILE
Tool activity renders only its name or paginated category; arguments and results are intentionally omitted. This reader is current-first and also handles legacy events; see data-model.md for older bare rollouts.
Resume, fork, and manage a found session
codex resume <SESSION_ID> # interactive; also accepts a thread name
codex resume --last # most recent for current directory
codex resume --all # picker across all directories
codex exec resume <SESSION_ID> "prompt" # headless continue (also --last / --all)
codex exec fork <SESSION_ID_OR_NAME> "prompt"
codex fork <SESSION_ID> # branch into a new thread (also --last / --all)
codex archive <SESSION> # move to archived_sessions/
codex unarchive <SESSION> # move back
codex delete <SESSION> --force # permanently remove (--force requires a UUID)
The codex resume picker filters to the current cwd and interactive sources by default; add --all and --include-non-interactive to see everything (exec/MCP/subagent sessions). codex exec resume --all selects across threads. codex agents lists agent work. Queue commands mutate pending submissions; do not use them merely to inspect history.
Tips
- Revert can create a new rollout generation whose filename UUID differs from logical
session_meta.id;state_5.threads.rollout_pathresolves the current generation. Paginated forks usehistory_baselineage/cutoff rather than replaying the parent. Archive/delete/unarchive operate across logical generations and descendants. - Filename timestamps are local time; in-file timestamps are UTC — a late-evening session can sit in the "wrong" date directory.
- Subagent/review/compact threads have object-valued
sourceinsession_meta; filter on it (or onthread_source) to separate human sessions from automation. On a multi-agent host they can outnumber human sessions. originatoris a free-form host string, not an enum:Codex Desktop,codex-tui,codex_cli_rs,codex_exec,codex_work_desktopall occur.codex exec --ephemeralruns with no persisted rollout at all — such runs leave nothing undersessions/.codex migrate-rolloutsis dry-run by default. It rewrites canonical JSONL only with--apply; there is no--dry-runflag. There is nocodex historysubcommand.
Verified against codex-cli 0.153.4 on 2026-09-06. A live persistent run wrote 15 ordinalled lines (0–14), including paginated UserMessage/AgentMessage and token_usage_record; Studio and laptop corpora were exhaustively checked. See data-model.md and the 0.146.0 historical snapshot.