Skip to content

Instantly share code, notes, and snippets.

@DovieW
Last active September 7, 2026 03:39
Show Gist options
  • Select an option

  • Save DovieW/fbe3e241c5983b5c96d1b56d3890449e to your computer and use it in GitHub Desktop.

Select an option

Save DovieW/fbe3e241c5983b5c96d1b56d3890449e to your computer and use it in GitHub Desktop.
Safety-first guide to migrating, restoring, repairing, and remotely accessing Codex CLI/Desktop threads

Migrating, restoring, and repairing Codex threads across computers

This is a safety-first field guide for moving and recovering Codex CLI/Desktop conversation history, making it visible to Codex Remote, and diagnosing the particularly confusing case where a thread exists on disk but does not appear in a client.

It covers two different layers:

  1. Supported Codex operations through the CLI and App Server protocol.
  2. Forensic recovery of local persisted state when supported operations can no longer discover an otherwise intact thread.

The second layer is deliberately conservative. Codex's on-disk schema is an implementation detail and can change between releases. Never begin there when thread/read, thread/resume, thread/fork, or thread/unarchive can solve the problem.

Privacy warning: a Codex rollout can contain prompts, responses, file paths, diffs, command output, tool input/output, images, account identifiers, and secrets accidentally exposed during a session. Treat it as sensitive data. Encrypt transfers, narrow file permissions, and never publish a real rollout as a troubleshooting sample.

What is a Codex thread?

The Codex App Server describes its hierarchy as:

  • A thread is a conversation.
  • A turn is one user request and the agent work that follows.
  • An item is a user/agent message, command execution, file change, tool call, or another unit inside a turn.

For local sessions, the durable record is normally a JSON Lines rollout under the user's Codex data directory. A local SQLite state database provides an index and mutable metadata. The CLI, Desktop app, and Remote clients do not simply enumerate SQLite rows: normal listing can scan rollout files and repair the index.

That distinction explains several otherwise contradictory symptoms:

  • The database contains the thread, but the Desktop app does not list it.
  • thread/read succeeds by ID, but thread/list omits the thread.
  • thread/list with useStateDbOnly: true finds it, while the default call does not.
  • A fresh App Server sees a repaired thread, but an already-running daemon or mobile client still shows the old list.

Supported behavior and compatibility boundary

Prefer the supported App Server operations:

  • thread/list discovers stored threads and supports pagination, source, archive, working-directory, and search filters.
  • thread/read reads a stored thread without loading or subscribing to it.
  • thread/resume loads an existing thread so a later turn can append to it.
  • thread/fork creates a new thread from stored history.
  • thread/archive and thread/unarchive move threads between active and archived collections.
  • thread/metadata/update updates documented mutable metadata such as pin and Git information.

The official protocol requires an initialize request followed by an initialized notification before other requests on a connection.

Codex Remote is a client of a Codex process running on the selected computer. Choosing the wrong host can therefore show a completely valid—but different— project and thread list. The host selector is part of the debugging surface, not decoration.

At the time of writing, the public Remote quickstart documents connected Mac and Windows computers. Linux host procedures may rely on preview or internal capabilities and should be treated as version-sensitive unless current official documentation says otherwise.

Storage anatomy

Typical local state looks conceptually like this:

~/.codex/
├── sessions/
│   └── YYYY/MM/DD/
│       └── rollout-<timestamp>-<thread-id>.jsonl
├── archived_sessions/
├── state_5.sqlite
├── thread_history_1.sqlite
└── ...

Names and schema versions can change. Discover them; do not hard-code them into an unattended migration tool.

A rollout is append-oriented JSONL. Common record classes include session_meta, turn_context, response_item, event_msg, and compaction or usage records. Important properties include:

  • The first usable session_meta identifies the thread and original runtime.
  • Ordering matters.
  • Some releases persist an ordinal; if present, it should remain unique and monotonic.
  • A user message may exist both as model-visible history and as a lifecycle event used by discovery/presentation.
  • The rollout's working directory influences project grouping.
  • SQLite is an index, not a safe substitute for the rollout.

Do not copy authentication files, tokens, or an entire Codex home merely to move one conversation. Copy only the required rollout and intentionally chosen non-secret metadata.

Choose the least invasive recovery path

Use this order:

  1. Confirm the correct account, workspace, computer, and project are selected.
  2. Confirm the source and destination run compatible/current Codex versions.
  3. Try normal thread/list and search without a restrictive cwd or source filter.
  4. Try thread/read by the known ID.
  5. If archived, use thread/unarchive.
  6. If readable, use thread/resume or thread/fork rather than rewriting the rollout.
  7. Only if the supported layer cannot discover an intact file, perform read-only forensic inspection.
  8. Back up, stop writers, make one narrow repair, validate with a fresh App Server, and retain rollback material.

Preflight checklist

On both computers, record versions and resolve the actual data directory:

codex --version
printf 'CODEX_HOME=%s\n' "${CODEX_HOME:-$HOME/.codex}"

Check for active writers before copying or editing:

pgrep -af 'codex|ChatGPT'

Pause active turns. Close or stop only the process that owns the target thread; do not kill every Codex process on a multi-session workstation. A copied file can be inconsistent if the source process appends during transfer.

Inventory candidate rollouts without printing their content:

CODEX_DATA="${CODEX_HOME:-$HOME/.codex}"
find "$CODEX_DATA/sessions" -type f \
  \( -name 'rollout-*.jsonl' -o -name 'rollout-*.jsonl.gz' \) \
  -printf '%TY-%Tm-%TdT%TH:%TM:%TS %s %p\n' | sort

Avoid piping real rollout contents into public paste services, issue trackers, AI tools, or shell history.

Back up correctly

Back up the exact rollout before every mutation:

rollout='/path/to/rollout-...jsonl'
backup="${rollout}.backup.$(date -u +%Y%m%dT%H%M%SZ)"
cp --preserve=mode,timestamps -- "$rollout" "$backup"
sha256sum -- "$rollout" "$backup"

For a broader recovery snapshot, stop the owning processes first and archive the sessions directory plus the state database:

CODEX_DATA="${CODEX_HOME:-$HOME/.codex}"
snapshot="$HOME/codex-recovery-$(date -u +%Y%m%dT%H%M%SZ).tar"
tar -C "$CODEX_DATA" -cf "$snapshot" sessions archived_sessions state_5.sqlite
chmod 600 "$snapshot"

Store this archive securely and delete it when no longer needed. It may contain the complete text of private work.

Migrating a thread to another computer

1. Prepare the destination workspace

Clone or copy the project first. Decide its canonical destination path, for example:

/home/example/work/project-a

The old path does not need to exist forever, but path differences matter for project grouping and for tools or messages that reference absolute paths. Moving a transcript does not move its repository, uncommitted changes, ignored files, credentials, tool configuration, or external services.

If the original thread used multiple source folders, note that a remote-project UI may expose only one primary folder even when the agent can access other authorized local paths. Do not infer filesystem access from sidebar structure.

2. Identify exactly one rollout

Prefer a thread ID obtained through thread/list or the source client. Confirm that the filename and the first session_meta agree. A safe summary script:

#!/usr/bin/env python3
import json
import pathlib
import sys

path = pathlib.Path(sys.argv[1])
with path.open("r", encoding="utf-8") as handle:
    first = json.loads(handle.readline())

payload = first.get("payload", {})
print({
    "record_type": first.get("type"),
    "thread_id": payload.get("id"),
    "cwd": payload.get("cwd"),
    "source": payload.get("source"),
    "history_mode": payload.get("history_mode"),
})

Do not print the rest of the file unless necessary.

3. Transfer over an authenticated encrypted channel

Place the rollout in the destination's active sessions tree. Preserve it as a separate file; do not concatenate rollouts.

src='/path/to/rollout-...jsonl'
dest_host='example-host'
dest_dir='~/.codex/sessions/YYYY/MM/DD/'

rsync -a --protect-args -- "$src" "$dest_host:$dest_dir"

Then compare hashes over SSH:

sha256sum -- "$src"
ssh "$dest_host" 'sha256sum -- ~/.codex/sessions/YYYY/MM/DD/rollout-...jsonl'

Use an SSH configuration alias instead of embedding usernames, addresses, or keys in reusable documentation.

4. Let Codex reconcile it

Start a fresh App Server or restart only the dedicated headless remote-control daemon. Do not restart an interactive Desktop app that owns unrelated active turns.

Run normal thread/list first. The default behavior scans rollout logs and can repair metadata; useStateDbOnly: true intentionally skips that scan.

5. Verify all layers

A successful migration requires all of these:

  • Normal thread/list returns the ID.
  • thread/read with turns succeeds.
  • The destination workspace path is correct.
  • The intended Desktop/Remote project lists the thread.
  • Opening it renders real history, not merely a title or empty shell.
  • Starting a new turn, if desired, writes to the destination thread rather than creating an accidental duplicate.

Do not call a migration complete after checking only SQLite or only the file.

A small App Server inspection client

The following script follows the documented initialization handshake and sends read-only requests. Save it locally as inspect_codex_threads.py.

#!/usr/bin/env python3
import json
import subprocess
import sys

proc = subprocess.Popen(
    ["codex", "app-server"],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    stderr=sys.stderr,
    text=True,
    bufsize=1,
)

def send(message):
    proc.stdin.write(json.dumps(message) + "\n")
    proc.stdin.flush()

send({
    "method": "initialize",
    "id": 1,
    "params": {
        "clientInfo": {
            "name": "local_recovery_inspector",
            "title": "Local recovery inspector",
            "version": "0.1.0",
        }
    },
})
send({"method": "initialized", "params": {}})
send({
    "method": "thread/list",
    "id": 2,
    "params": {"limit": 100, "sortKey": "recency_at"},
})

for line in proc.stdout:
    message = json.loads(line)
    if message.get("id") != 2:
        continue
    for thread in message.get("result", {}).get("data", []):
        # Deliberately omit preview text: it may contain private prompt content.
        print(json.dumps({
            "id": thread.get("id"),
            "name": thread.get("name"),
            "cwd": thread.get("cwd"),
            "status": thread.get("status"),
            "createdAt": thread.get("createdAt"),
            "updatedAt": thread.get("updatedAt"),
        }))
    break

proc.terminate()

To read a known thread without resuming it, replace the thread/list request with:

send({
    "method": "thread/read",
    "id": 2,
    "params": {"threadId": "THREAD_ID", "includeTurns": True},
})

Be aware that printing the response would expose the transcript. Validate structure or counts instead of dumping it to a shared terminal log.

Diagnosis matrix

Symptom Likely layer First checks
Host is absent or marked offline Remote transport/daemon Host power, network, account/workspace, Remote enabled, daemon status
Host is online but shows the wrong projects Host selection or working-directory grouping Explicitly select the intended host; compare canonical cwd values
File exists but all listing methods omit it Corrupt/unsupported rollout head Parse every JSONL line; inspect first session_meta; compare version/history mode
useStateDbOnly lists it but normal listing does not Rollout discovery disagrees with SQLite Inspect early preview-bearing user lifecycle events; do not “fix” SQLite first
Fresh App Server sees it, long-running daemon does not In-memory daemon state Restart only the dedicated daemon after active turns are paused
Daemon sees it, mobile still does not Mobile/UI cache or selected host Switch hosts; if unchanged, restart only the mobile app process without clearing data
Thread title appears but opening is blank Read/materialization failure thread/read(includeTurns=true), JSONL parse, history-mode compatibility
Side chat fails and logs say expected ordinal N, got M Stale paginated-history projection Stop writers; compare the projection checkpoint with the rollout record at that byte offset
Sidebar shows a spinner after work stopped Stale loaded/active state or disconnected client Check thread/loaded/list, runtime status, daemon logs, and last turn-completion event
Model list differs between local and remote Different runtime/version/account policy Query versions on both computers and restart the process still running the old binary
Thread appears under the wrong project Stored/runtime cwd mismatch Verify canonical destination path and client project definition

The hidden-thread failure: SQLite finds it, normal listing does not

This case deserves special treatment because it looks like data loss when it is usually an indexing problem.

In the inspected Codex source revision, filesystem discovery reads the head of each rollout and requires:

  1. a session metadata record; and
  2. a discoverable preview.

The preview is derived from certain lifecycle events, including a user-message event or a completed user-message item. A raw response_item whose role is user can preserve the model-visible transcript without satisfying this discovery predicate.

The inspected revision scans a small normal head plus a bounded extension while looking for the preview. Consequently, a migrated rollout can be fully valid and readable but invisible to normal thread/list if its first recognized user-message lifecycle event occurs too late. SQLite may still retain a title and preview from earlier indexing, explaining why useStateDbOnly: true finds the same thread.

This is version-specific behavior, not a promised storage contract. Inspect the source matching the installed Codex version before applying any repair.

Read-only detector

This detector reports whether an early completed user-message event exists. It does not print message text.

#!/usr/bin/env python3
import json
import pathlib
import sys

path = pathlib.Path(sys.argv[1])
limit = int(sys.argv[2]) if len(sys.argv) > 2 else 250

saw_meta = False
saw_user_response = False
saw_user_event = False
errors = []

with path.open("r", encoding="utf-8") as handle:
    for number, line in enumerate(handle, 1):
        try:
            record = json.loads(line)
        except Exception as exc:
            errors.append((number, str(exc)))
            continue

        payload = record.get("payload", {})
        if record.get("type") == "session_meta":
            saw_meta = True
        if (
            record.get("type") == "response_item"
            and payload.get("type") == "message"
            and payload.get("role") == "user"
        ):
            saw_user_response = True
        if (
            number <= limit
            and record.get("type") == "event_msg"
            and payload.get("type") == "item_completed"
            and payload.get("item", {}).get("type") == "UserMessage"
        ):
            saw_user_event = True

print({
    "session_meta": saw_meta,
    "user_response_anywhere": saw_user_response,
    "completed_user_event_in_head": saw_user_event,
    "parse_error_count": len(errors),
})

Last-resort repair principles

If—and only if—the installed source confirms this exact mismatch:

  1. Stop the thread owner and dedicated remote daemon.
  2. Make a byte-for-byte backup and hash it.
  3. Locate the first real user response_item and its turn identifier.
  4. Derive the missing completed-user lifecycle event from that existing message. Do not invent or rewrite conversation text.
  5. Insert it adjacent to the source message.
  6. If the rollout uses ordinals, increment subsequent ordinals atomically so they remain unique and monotonic.
  7. Write a new file in the same directory, fsync, preserve permissions, and atomically replace the target.
  8. Parse every line and verify ordering.
  9. Start a fresh App Server and confirm normal thread/list, then thread/read(includeTurns=true).
  10. Restart only the required UI/daemon layers and verify the real client.

Do not publish a universal mutation script for this operation. Exact event schemas, capitalization, IDs, timestamp fields, and history modes can differ by version. A script that is correct for one build can silently damage another.

Paginated-history projection failures

Some Codex builds keep a second SQLite database for paginated turn and item history. In one observed schema it was named thread_history_1.sqlite and contained tables similar to:

  • thread_history_projection_state
  • thread_turns
  • thread_items
  • thread_realtime_items

These are derived data. The rollout remains the durable conversation record, but a stale projection can prevent history pagination, opening a side chat, or showing recent turns. A characteristic App Server error is:

thread history projection for THREAD_ID expected ordinal N, got M

This often occurs after a forensic rollout repair changes record ordinals or byte positions without rebuilding the projection. It is not evidence that the conversation text is gone.

Read-only checkpoint audit

Inspect the schema from the installed build first. If it has the fields shown below, this script compares each projection checkpoint with the rollout record at the checkpoint byte offset. It prints no prompts or responses.

#!/usr/bin/env python3
import json
import os
import pathlib
import sqlite3

codex_home = pathlib.Path(os.environ.get("CODEX_HOME", pathlib.Path.home() / ".codex"))
state_path = codex_home / "state_5.sqlite"
history_path = codex_home / "thread_history_1.sqlite"

state = sqlite3.connect(f"file:{state_path}?mode=ro", uri=True)
state.row_factory = sqlite3.Row
history = sqlite3.connect(f"file:{history_path}?mode=ro", uri=True)

query = """
select id, rollout_path, archived
from threads
where rollout_path is not null
order by updated_at desc
"""

for thread in state.execute(query):
    rollout = pathlib.Path(thread["rollout_path"])
    if not rollout.is_file():
        continue

    checkpoint = history.execute(
        """
        select next_rollout_byte_offset, next_rollout_ordinal
        from thread_history_projection_state
        where thread_id = ?
        """,
        (thread["id"],),
    ).fetchone()
    if checkpoint is None:
        continue

    offset, expected = checkpoint
    size = rollout.stat().st_size
    if offset == size:
        continue

    boundary = True
    actual = None
    try:
        with rollout.open("rb") as handle:
            if offset:
                handle.seek(offset - 1)
                boundary = handle.read(1) == b"\n"
            handle.seek(offset)
            actual = json.loads(handle.readline()).get("ordinal")
    except (OSError, json.JSONDecodeError):
        boundary = False

    if not boundary or actual != expected:
        print({
            "thread_id": thread["id"],
            "archived": bool(thread["archived"]),
            "rollout_size": size,
            "checkpoint_offset": offset,
            "expected_ordinal": expected,
            "actual_ordinal": actual,
            "record_boundary": boundary,
        })

An offset behind end-of-file is not automatically corrupt. If the record at that offset has the expected ordinal, the projection may simply be valid but not yet materialized. A mismatch, a non-record boundary, or the corresponding App Server error is stronger evidence.

Safe rebuild procedure

Do not rebuild while any process can append to an affected rollout.

  1. Identify exact non-archived target thread IDs. Exclude archived duplicates unless they are independently needed.
  2. Verify the final rollout lifecycle event. A terminal task_complete or turn_aborted is useful evidence, not proof by itself; also confirm no process owns the thread and that the file size remains stable.
  3. Parse every JSONL line and verify ordinals from beginning to end.
  4. Back up the history database together with its -wal and -shm files. Also create a consistent SQLite backup using SQLite's backup API.
  5. Hash every affected rollout. Copy any rollout that will require mutation.
  6. If the raw rollout is already contiguous, do not change it.
  7. If one inserted record duplicates the preceding ordinal, inspect the local context and installed parser. Only after proving that exact defect, shift the inserted record and every later ordinal by one using an atomic rewrite.
  8. Re-parse the entire rewritten rollout and prove ordinals are contiguous.
  9. In one transaction, delete only the affected thread IDs from the derived projection tables. Never drop the database or delete unrelated rows.
  10. Let the matching Codex App Server rebuild each projection. Process large rollouts independently so memory is released between threads.
  11. Verify each projection's next byte offset equals the rollout's exact size, the next ordinal equals the record count, and PRAGMA integrity_check returns ok.

One safe way to ask App Server to exercise the paginated materializer without creating a persistent side chat is an ephemeral fork:

{
  "method": "thread/fork",
  "id": 2,
  "params": {
    "threadId": "THREAD_ID",
    "ephemeral": true,
    "excludeTurns": true
  }
}

Enable capabilities.experimentalApi during initialization. Confirm the returned thread is ephemeral and has no persisted path. This is a validation technique, not a promise that future Codex builds will keep the same internal projection schema.

SQLite: useful for diagnosis, dangerous as the source of truth

Reading the state database can answer:

  • Does an index row exist?
  • What rollout path, title, archive flag, or working directory is indexed?
  • Does database-only listing disagree with filesystem discovery?

Avoid direct database writes. Normal listing may overwrite them during reconciliation, and schema constraints can change. Prefer documented App Server metadata methods. If recovery absolutely requires SQLite work:

  • stop every process using that Codex home;
  • back up the database, -wal, and -shm files together;
  • inspect the schema from that exact build;
  • use a transaction;
  • never delete unrelated rows;
  • validate through App Server afterward;
  • retain rollback files until the UI is verified.

Deleting the state database to “force a rebuild” is a broad destructive action, not a first-line repair.

Working-directory and project repair

A recovered thread may be healthy yet appear under the wrong project because its historical cwd names a path from the source computer or a no-longer- existing worktree.

Prefer these options:

  1. Recreate the same path when that is natural.
  2. Create a narrowly scoped compatibility symlink when the old path is safe and unambiguous.
  3. Resume or fork through supported APIs with the intended destination context.
  4. As a forensic last resort, adjust only the first session metadata record, after checking the installed parser and retaining a backup.

Never rewrite every historical cwd. Turn contexts and command records are a historical account of where work actually ran. Changing all of them makes the record misleading and can corrupt tool presentation.

Remember that thread migration and worktree migration are separate:

git -C /path/to/project status --short --branch
git -C /path/to/project worktree list --porcelain

Preserve uncommitted and untracked work independently. A perfect transcript is not a repository backup.

Remote troubleshooting

Treat Remote as a chain:

mobile client
  -> account/workspace authorization
  -> selected computer
  -> remote-control transport
  -> Codex App Server process
  -> thread discovery/index
  -> rollout materialization
  -> destination workspace

Test from the bottom up:

  1. Is the destination project directory present and readable?
  2. Does a fresh local App Server list and read the thread?
  3. Does the dedicated remote daemon run the expected Codex binary/version?
  4. Does the daemon reconnect after restart?
  5. Is the mobile app explicitly showing the intended host?
  6. Does a cold mobile-app reopen refresh the list?
  7. Can the thread open and render its latest items/diff?

Do not equate a green host dot with correct history. It proves transport health, not that indexing or project grouping is correct.

Desktop-owned and headless ownership

There are two different operational shapes that are easy to conflate:

  1. Desktop-owned host: ChatGPT Desktop owns its Codex App Server and Remote Control connection. Closing Desktop takes that host offline. This is the behavior documented for normal connected Mac and Windows computers.
  2. Headless host: a service manager owns a dedicated Codex App Server on an always-on or SSH development machine. This is an operational deployment choice and must be managed explicitly; exact commands remain version-sensitive.

Choose one owner for one persisted Remote Control enrollment. Do not run a Desktop-owned server and a headless server against the same identity at the same time. A common symptom is repeated HTTP 409 responses containing:

Remote app server already online

The safe recovery is not a broad pkill:

  1. List exact App Server PIDs, parents, start times, and command lines.
  2. Determine which process is the intended durable owner.
  3. Confirm affected turns are terminal or explicitly pause them.
  4. Stop only the losing owner with a normal termination signal.
  5. Leave the intended owner running and allow the stale backend lease to clear; repeated restarts can prolong the collision.
  6. Verify an explicit Connected status or connection log from the intended process, then verify the real mobile/Desktop client.

Persisting a Remote Control preference is not the same as ensuring a process exists to honor it. Conversely, a running App Server does not prove it owns the current remote connection.

Safe process discipline

  • Pause active turns before daemon maintenance.
  • Identify process ownership and exact command lines.
  • Restart the narrow dedicated service, not every Codex/Desktop process.
  • Never use a broad pkill codex on a workstation with concurrent sessions.
  • Confirm the new daemon uses the expected executable; updating a binary does not replace an already-running process.
  • After server repair, a client may still cache the old list. Restarting only the client process is safer than clearing application data.

Validation after recovery

The minimum acceptance test is:

[ ] Source rollout backup exists and hashes match the pre-edit file
[ ] Every JSONL line parses
[ ] First session metadata ID matches the target thread
[ ] Ordinals are unique and monotonic when present
[ ] Normal thread/list returns the thread
[ ] Database-only and normal listing no longer contradict each other
[ ] thread/read with turns returns real history
[ ] Correct canonical workspace is present
[ ] Correct host and project show the thread
[ ] Opening the thread renders messages and recent work
[ ] A new turn is appended only when intentionally tested
[ ] Repository/untracked work is independently intact
[ ] Temporary recovery copies are secured or removed

For a thread ending in an interrupted turn, confirm that the interruption has a terminal event. An interrupted last turn is not, by itself, evidence that the whole thread is corrupt.

Common mistakes

  • Copying only SQLite: the index points at a rollout that is absent.
  • Copying all of ~/.codex: unnecessarily transfers credentials and other private conversations.
  • Editing while Codex is running: the writer can append to or replace your repaired file.
  • Trusting a title-only UI check: the transcript may still fail to load.
  • Fixing the database first: filesystem reconciliation can undo the change.
  • Changing thread IDs casually: references, fork lineage, and filename identity can diverge.
  • Flattening fork metadata: it may make a thread look standalone while losing real lineage.
  • Rewriting all absolute paths: historical tool records become false.
  • Assuming an update is active: the daemon may still be the old process.
  • Confusing host status dots: a horizontally scrolling host selector can visually associate a dot with the neighboring computer.
  • Clearing mobile app data: this is rarely necessary and may sign the user out or remove unrelated local state.
  • Publishing diagnostic output: previews and tool logs can expose private data even when tokens are redacted.

Preventive practices

For important long-running work:

  • Keep repositories committed or separately backed up; transcripts are not source control.
  • Record the thread ID and canonical project path in a private recovery note.
  • Keep Codex versions reasonably aligned across machines.
  • Update the executable and restart its owning daemon as one managed operation.
  • Use a service manager for a headless remote daemon, with a narrow unit and explicit executable path.
  • Monitor connection/restart state without logging prompt or transcript bodies.
  • Periodically validate that a normal thread list—not just SQLite—can discover critical sessions.
  • Prefer supported fork/resume operations for planned moves.
  • Keep recovery tooling read-only by default and require an explicit target path for mutation.
  • Never build automation that recursively deletes or rewrites the Codex data directory.

Evidence and references

The low-level observations in this guide were validated against open-source revision ac192cd7937b0d73edc6dffe009940ae53782dd4. Re-check the corresponding source before using any forensic procedure with another version.

Final rule

Restore evidence before restoring convenience. Preserve the original rollout, prove which layer is failing, change one invariant at a time, and verify the result through the same client the user actually depends on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment