Skip to content

Local Codex and Claude Conversation Data

Codex Desktop and Claude's local coding-agent workflows persist readable conversation records on macOS. These files can be inspected or archived without requesting a full account export and without copying browser profiles.

This guide covers the stores verified on August 26, 2026. Vendor formats are not public compatibility contracts, so paths and record shapes may change after application updates.

What Is Available Locally

Codex Desktop and CLI

The primary transcript store is:

~/.codex/sessions/YYYY/MM/DD/rollout-<timestamp>-<id>.jsonl

Each file is newline-delimited JSON. Common top-level record types include:

  • session_meta: thread ID, parent thread, working directory, originator, and source information.
  • response_item: user and assistant messages, reasoning, and tool calls.
  • event_msg: task lifecycle, token, and tool-status events.
  • turn_context and world_state: execution context and permissions.
  • compacted: replacement history created when a long thread is compacted.

Thread titles are indexed separately:

~/.codex/session_index.jsonl

Codex also maintains SQLite projections such as ~/.codex/thread_history_1.sqlite and ~/.codex/state_5.sqlite. They are useful for structured queries, but rollout JSONL is the simplest source for a complete, portable transcript. Files such as auth.json and config.toml are not conversation data and should not be copied into an archive.

Codex creates additional rollout files for guardian reviews and other subagents. The newest file by modification time is therefore not always the newest human-facing thread. A main thread normally has session_meta.payload.thread_source set to user; a subagent has a parent_thread_id and a non-user source such as guardian_review.

Claude Desktop Coding Sessions and Claude Code

The canonical readable transcript store used by Claude Code and Claude Desktop's local coding-agent sessions is:

~/.claude/projects/<encoded-project>/<session-id>.jsonl

The project directory encodes the original working directory. For example:

-Users-example-GitHub-project

Main sessions are stored directly in that directory. Agent children are usually under a subagents/ directory.

Useful record types include:

  • queue-operation with operation: "enqueue": a prompt submitted by the person using Claude.
  • user and assistant: normalized conversation records with a nested message object.
  • Tool-use and tool-result records.
  • Session, bridge, attachment, and queue metadata.

~/.claude/history.jsonl may contain interactive command history, but it is not a substitute for project transcripts.

Claude Desktop also writes metadata under:

~/Library/Application Support/Claude/

The claude-code-sessions/ JSON files found there can identify a corresponding cliSessionId, but they do not necessarily contain message bodies; sessions marked transcriptUnavailable have none locally. Local Cowork sessions keep their own Claude Code home, so their full transcripts are under local-agent-mode-sessions/<account>/<org>/local_<id>/.claude/projects/ and use the same JSONL format. Cloud sessions (claude.ai/code and remote Cowork) and ordinary claude.ai chats are not stored locally in full; Chromium IndexedDB or LevelDB holds only fragments. claude.ai rejects automated browsers, so signed-in browser capture is not available for it. Instead:

  • Import a Claude account export (Settings → Privacy → Export data) with aesop import claude_web ~/Downloads/<export>.zip for claude.ai chats.
  • Bring a cloud coding session to this Mac with claude --teleport <session-id> from a checkout of its repository. The resumed session is saved as Claude Code project JSONL, which the claude_code collector archives.

macOS Access

Reading files in ~/.codex and ~/.claude normally requires only access to the current user's home directory. If macOS denies access:

  1. Open System Settings → Privacy & Security → Full Disk Access.
  2. Grant access to the terminal or Python executable performing the read.
  3. Quit and reopen that application after changing the permission.

JSONL rollouts are append-only in normal use. Reading them while an agent is working is generally safe, but the final line may still be in progress. Wait for the current response to finish before copying a stable snapshot. Close the source app before directly copying mutable SQLite, IndexedDB, or LevelDB stores.

Locate the Newest Main Codex Thread

This read-only script ignores subagent rollouts and prints the most recently modified user thread:

python3 - <<'PY'
import json
from pathlib import Path

candidates = []
for path in Path.home().glob(".codex/sessions/*/*/*/*.jsonl"):
    try:
        with path.open(encoding="utf-8") as handle:
            first = json.loads(handle.readline())
    except (OSError, json.JSONDecodeError):
        continue
    payload = first.get("payload", {})
    if first.get("type") == "session_meta" and payload.get("thread_source") == "user":
        candidates.append(path)

if not candidates:
    raise SystemExit("No main Codex rollout was found.")

print(max(candidates, key=lambda path: path.stat().st_mtime))
PY

To resolve a thread title, read ~/.codex/session_index.jsonl and select the last entry whose id matches the rollout's session ID. The index can contain multiple titles for one thread as Codex renames it.

Read the Last Human Prompt From Codex

The following script selects the newest main rollout and prints the last top-level user message. Requiring a top-level response_item avoids mistaking older messages embedded in a compacted.replacement_history record for the latest prompt.

python3 - <<'PY'
import json
from pathlib import Path

main_threads = []
for path in Path.home().glob(".codex/sessions/*/*/*/*.jsonl"):
    try:
        with path.open(encoding="utf-8") as handle:
            first = json.loads(handle.readline())
    except (OSError, json.JSONDecodeError):
        continue
    payload = first.get("payload", {})
    if first.get("type") == "session_meta" and payload.get("thread_source") == "user":
        main_threads.append(path)

path = max(main_threads, key=lambda item: item.stat().st_mtime)
latest = None
with path.open(encoding="utf-8") as handle:
    for line in handle:
        try:
            event = json.loads(line)
        except json.JSONDecodeError:
            continue
        payload = event.get("payload", {})
        if (
            event.get("type") == "response_item"
            and payload.get("type") == "message"
            and payload.get("role") == "user"
        ):
            text = [
                block.get("text", "")
                for block in payload.get("content", [])
                if block.get("type") == "input_text"
            ]
            if text:
                latest = "\n".join(text)

print(latest or "No user prompt was found.")
PY

Locate the Newest Main Claude Coding Session

This script excludes child agents and prints the newest project transcript:

python3 - <<'PY'
from pathlib import Path

paths = [
    path
    for path in Path.home().glob(".claude/projects/**/*.jsonl")
    if "subagents" not in path.parts
]
if not paths:
    raise SystemExit("No Claude project transcript was found.")
print(max(paths, key=lambda path: path.stat().st_mtime))
PY

Modification time is a practical discovery hint, not authoritative chronology. Use record timestamps when comparing sessions that may have been copied, restored, or touched by a later process.

Read the Last Human Prompt From Claude

For local coding sessions, the last non-empty enqueued queue operation is the least ambiguous representation of the last submitted prompt:

python3 - <<'PY'
import json
from pathlib import Path

paths = [
    path
    for path in Path.home().glob(".claude/projects/**/*.jsonl")
    if "subagents" not in path.parts
]
path = max(paths, key=lambda item: item.stat().st_mtime)
latest = None
with path.open(encoding="utf-8") as handle:
    for line in handle:
        try:
            event = json.loads(line)
        except json.JSONDecodeError:
            continue
        if (
            event.get("type") == "queue-operation"
            and event.get("operation") == "enqueue"
            and event.get("content")
        ):
            latest = event["content"]

print(latest or "No enqueued user prompt was found.")
PY

Claude may also encode tool results as user-role messages. Using queue-operation prevents those machine-generated records from being reported as human prompts.

Archive and Render With Aesop

From the Aesop-Agentic repository:

aesop collect --source codex --json
aesop parse --source codex --json

aesop collect --source claude_code --json
aesop parse --source claude_code --json

Snapshots appear under:

data/raw/codex/
data/raw/claude_code/

Human-readable documents appear under:

data/parsed/codex/
data/parsed/claude_code/

The source stores remain untouched. Aesop's explicit allowlists omit Codex authentication files and Claude credential files. Run local verification after collection and parsing:

aesop verify --json

This command also checks configured backup destinations. A destination mismatch means the local archive has changed since its last mirror; it does not by itself mean the source snapshot or parsed document is corrupt.

Known Limitations

  • Codex thread titles live in a separate index and are not always merged into the rendered title.
  • Codex compacted replacement history requires special handling to reconstruct prior turns without duplicates.
  • Codex SQLite projections may contain records not decoded by the current parser; their raw snapshots are retained as opaque coverage.
  • Claude Desktop metadata can point to a Claude Code session without containing transcript bodies.
  • Ordinary web chats from claude.ai are outside the local coding-session workflow and may require a separate supported export or authenticated capture.
  • Local files can include private prompts, source code, tool output, paths, and secrets printed during a session. Keep snapshots in private, access-controlled storage.