SKILL.md
Exploring Codex Sessions
Set CODEXROOT="${CODEXHOME:-$HOME/.codex}" and CODEXDBROOT="${CODEXSQLITEHOME:-$CODEXROOT}". Codex CLI stores sessions as JSONL rollouts under $CODEXROOT; 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 | |
$CODEXROOT/archivedsessions/rollout-*.jsonl |
Archived sessions (flat, no date dirs, via codex archive) |
|
$CODEXDBROOT/state_5.sqlite |
threads table: metadata index (cwd, preview, firstusermessage, title, name, git info, history_mode, archived flag). Treat as cache — rebuilt from rollouts |
|
$CODEXDBROOT/threadhistory1.sqlite |
Rebuildable transcript-item projection; contains item JSON but is not canonical | |
$CODEXDBROOT/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) |
|
$CODEXROOT/sessionindex.jsonl |
Thread names: {"id", "threadname", "updatedat"}, 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 CODEXSQLITEHOME, then CODEXHOME; do not assume CODEXHOME 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](data-model.md):
sessionmeta— line 1:id/sessionid,cwd,cliversion,source(cli,vscode,exec,mcp, subagent objects),git{branch, commithash, repositoryurl},historymode,contextwindow,forkedfrom_idresponseitem— model-visible conversation:message(rolesuser/assistant/developer),functioncall(+output),customtoolcall(+output, e.g.applypatch),toolsearchcall/toolsearchoutput,agentmessage(cross-agent delivery),reasoning(encrypted),websearchcalleventmsg— legacy UI eventsusermessage/agentmessage; paginateditemcompletedcarriesUserMessage/AgentMessageTurnItems. Exclude commentary.turncontext— per-turn snapshot:model,cwd,approvalpolicy,sandboxpolicy,permissionprofile,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 replayinteragentcommunication/_metadata— multi-agent only
Injected-context pitfall: responseitem user and developer messages carry harness injections. In legacy format prefer eventmsg/usermessage; in paginated format prefer itemcompleted/UserMessage. Do not treat bare-era files as covered by these envelope recipes.
Format and integrity pitfall: current paginated files use itemcompleted; legacy files use usermessage/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 threadsource='user' to drop subagent/guardian threads, whose firstuser_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 state5.threads.firstuser_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](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
sessionmeta.id;state5.threads.rolloutpathresolves the current generation. Paginated forks usehistorybaselineage/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
sourceinsessionmeta; filter on it (or onthreadsource) 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,codexclirs,codexexec,codexwork_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 tokenusagerecord; Studio and laptop corpora were exhaustively checked. See [data-model.md](data-model.md) and the [0.146.0 historical snapshot](references/data-model-0.146.0.md).