Syncing Codex Settings and Conversation History Across Multiple Macs
Sync Codex settings and conversation JSONL files across Macs with Syncthing, then repair each machine’s local sidebar index automatically.
I use Codex on both a MacBook and a Mac Mini. I wanted not only the same skills and settings on both machines, but also the ability to continue a conversation started on either one.
I added Codex's .codex directory to Syncthing using the same approach I had used for syncing Claude Code settings across multiple Macs.
The files synchronized correctly, but sessions created on the other machine still did not appear in the left sidebar.
Syncthing reported that everything was up to date, yet a Codex session created on the Mac Mini was missing from the MacBook sidebar.
- Conversation data lives in
~/.codex/sessions/**/*.jsonl. - The sidebar reads each machine's local
~/.codex/state_5.sqliteindex. - The JSONL file had arrived, but its session ID was absent from the receiving machine's SQLite database.
This post explains how I synchronize Codex settings and conversation JSONL files in both directions, then automatically repair each machine's local sidebar index as soon as Syncthing finishes receiving a file.
What lives inside ~/.codex
Codex keeps its settings and local task data under ~/.codex/.
| Path | Description | Sync |
|---|---|---|
AGENTS.md |
Global instructions | Yes |
config.toml |
Codex configuration | Yes |
rules/ |
Execution rules | Yes |
skills/ |
Custom skills | Yes |
automations/ |
Automation settings | Yes |
sessions/ |
Active task JSONL files | Yes |
archived_sessions/ |
Archived task JSONL files | Yes |
history.jsonl |
Input history | Yes |
session_index.jsonl |
Auxiliary session index | Yes |
attachments/ |
Task attachments | Yes |
state_5.sqlite* |
Machine-specific sidebar and runtime state | No |
auth.json and other credentials |
Machine-specific authentication | No |
| Caches, logs, and plugin runtimes | Re-creatable or machine-specific | No |
When I synchronized Claude Code settings, I excluded sessions. This time, continuing Codex tasks across machines was the point, so I included both sessions/ and archived_sessions/.
Why not synchronize state_5.sqlite directly?
Synchronizing SQLite would seem to make both sidebars identical automatically. I did not like that approach.
While Codex is running, SQLite may use all three of these files:
state_5.sqlite
state_5.sqlite-wal
state_5.sqlite-shm
- Syncthing does not transfer these three files as one atomic unit.
- Both machines may modify the database at the same time.
That can create conflict files or cause one machine's changes to overwrite the other's.
The safer design is to synchronize only the JSONL source records and let each machine rebuild its own SQLite index.
The actual problem
A Codex session created on the Mac Mini had already synchronized its JSONL record to the MacBook, but the session did not appear in the MacBook sidebar.
The JSONL file on the MacBook was byte-for-byte identical to the one on the Mini, and .stignore was not excluding it. The ID existed in the Mini's state_5.sqlite, but not in the MacBook's database.
Syncthing had finished its job. Codex simply had not registered the newly received JSONL file in the local sidebar index.
Running codex resume [SESSION_ID] could reopen the session. But finding an ID and manually resuming it every time is not synchronization. It is manual recovery, which was not what I wanted.
Syncthing setup
1. Install Syncthing
Install and run Syncthing on both machines with Homebrew.
brew install syncthing
brew services start syncthing
The web UI is available at http://localhost:8384 by default.
2. Connect the two machines
Copy the Device ID from one machine and add it as a Remote Device on the other. Approve the connection request to begin bidirectional synchronization.
3. Register ~/.codex as a shared folder
Use the same Folder ID on both machines.
Folder Label: codex-config
Folder ID: codex-config
Folder Path: /Users/<username>/.codex
Folder Type: Send & Receive
4. Use an allowlist-style .stignore
The .codex directory mixes portable configuration with credentials and machine-specific runtime state. An allowlist is safer than continually expanding an exclusion list.
// Codex history + portable setup
!config.toml
!AGENTS.md
!rules
!rules/**
!skills
!skills/**
!automations
!automations/**
!sessions
!sessions/**
!archived_sessions
!archived_sessions/**
!history.jsonl
!session_index.jsonl
!attachments
!attachments/**
// Ignore everything else
*
.stignore is a machine-local Syncthing control file, so apply the same contents separately on both machines. With this setup, state_5.sqlite, its WAL and SHM files, credentials, caches, and logs are excluded by the final *.
Repairing a missing sidebar entry after JSONL arrives
Codex App Server's thread/list method supports the useStateDbOnly option.
true: return only tasks already registered in SQLite.false: scan JSONL task logs and repair missing metadata.
There is no need to modify SQLite directly. Ask Codex itself to scan the JSONL files again.
The essential request looks like this:
{
"method": "thread/list",
"id": 1,
"params": {
"limit": 100,
"archived": false,
"sourceKinds": ["cli", "vscode", "appServer"],
"useStateDbOnly": false
}
}
Paginating through both active and archived tasks registers JSONL-only tasks in the local state database. The operation is safe to repeat and never writes to SQLite directly.
Choosing the right trigger
The trigger required more thought than the repair script itself.
First attempt: a five-minute LaunchAgent
My first version ran once at login, watched session_index.jsonl, and rescanned every five minutes in case it missed an event.
It was reliable, but imprecise. A file could arrive and still wait until the next interval, while a full scan ran even when nothing had changed.
More importantly, I disliked wasting resources for no reason.
Second attempt: Codex SessionStart and SessionEnd
Codex can run hooks at session start and end. These hooks still did not match the event I needed.
The relevant event is not "a Codex session started." It is "Syncthing finished receiving a remote JSONL file." Starting or closing the app does not correspond to file arrival.
An app launch or shutdown hook would have made the solution much simpler, but Codex does not currently provide one.
Final approach: Syncthing ItemFinished events
Syncthing provides a REST Event API. ItemFinished is emitted after a newer file version finishes synchronizing.
GET /rest/events?events=ItemFinished
A listener on each machine long-polls this endpoint and handles only events matching all four conditions:
- The Folder ID is
codex-config. erroris empty.- The event is an
updateto file contents. - The path matches
sessions/**/*.jsonlorarchived_sessions/**/*.jsonl.
Several files may arrive in quick succession, so the listener briefly debounces them and runs one repair. It also performs a complete repair when it starts, covering events missed while the listener was offline. A file lock prevents automatic and manual reconciliation from overlapping.
The final flow is:
Create a task on the Mini
→ Syncthing sends the JSONL file to the MacBook
→ The MacBook ItemFinished listener receives the event
→ MacBook Codex App Server scans the JSONL histories
→ The MacBook repairs its local SQLite index
→ Reopen Codex to show the task in the MacBook sidebar
Tasks created on the MacBook follow the same flow in reverse
Limitation: the running app's UI cache
Even after this automation, an already-running Codex app does not refresh its sidebar immediately. JSONL synchronization and local SQLite index repair finish automatically, but the task list already held in memory does not appear to learn that an external process updated the index.
I could not find an official command or supported interface for forcing a sidebar refresh. To see a newly synchronized task in the sidebar, I currently need to close and reopen Codex.
It is not fully real-time synchronization, but it is good enough for how I work. Reopening Codex after working on the other machine shows the latest sessions, and I am happy with that result.
LaunchAgent configuration
Syncthing itself already runs as a Homebrew LaunchAgent on both machines. I installed the event listener as a separate LaunchAgent instead of modifying Homebrew's service file. This survives brew upgrade and makes it possible to restart and inspect Syncthing and the listener independently.
Bidirectional task synchronization requires installing this LaunchAgent on both Macs because each receiving machine must register incoming JSONL files in its own local index.
The complete implementation is available in the public reconcile-codex-threads repository. Automatic repair uses these three files:
codex_thread_sync_listener.py: waits for SyncthingItemFinishedevents.codex_thread_index_reconciler.py: repairs the local index through Codex App Server.install_launch_agent.py: installs a LaunchAgent using the current user's paths.
The essential LaunchAgent configuration is:
<key>Label</key>
<string>local.reconcile-codex-threads.listener</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>/Users/<username>/.local/libexec/codex_thread_sync_listener.py</string>
<string>--syncthing-config</string>
<string>/Users/<username>/Library/Application Support/Syncthing/config.xml</string>
<string>--reconciler</string>
<string>/Users/<username>/.local/libexec/codex_thread_index_reconciler.py</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
The listener waits for Syncthing events and runs only when a relevant JSONL file arrives. It reads the API key from each machine's Syncthing config.xml at runtime.
Manual reconciliation skill
I also created $reconcile-codex-threads as a manual inspection path. It performs the following steps:
- Request an immediate rescan of the
codex-configfolder on both machines. - Reconcile both local indexes from JSONL histories.
- Retrieve both state-only sidebar inventories through App Server.
- Compare task IDs and archive state.
- Report titles and IDs that exist on only one machine.
The controller can also be run directly:
/usr/bin/python3 \
"${CODEX_HOME:-$HOME/.codex}/skills/reconcile-codex-threads/scripts/reconcile_codex_threads.py"
The exact filename is reconcile_codex_threads.py. It does not transfer files itself. It requests a Syncthing rescan, repairs both local indexes, and compares the results.
On the machine used to run the controller, create the non-synchronized local configuration file ~/.config/reconcile-codex-threads/config.json and specify the other Mac's SSH alias.
{
"ssh_host": "other-mac"
}
A converged result looks like this:
{
"status": "converged",
"local": {
"thread_count": 277,
"active_count": 268,
"archived_count": 9
},
"remote": {
"thread_count": 277,
"active_count": 268,
"archived_count": 9
},
"only_local": [],
"only_remote": [],
"archive_state_mismatches": []
}
This skill is useful not only for repairing a broken automation path, but also for checking whether both sidebar indexes actually agree.
Lessons learned
JSONL and the sidebar are different states
A file existing on disk does not guarantee that it will appear in the sidebar. The conversation record and the local display index must be diagnosed separately.
Run one complete repair when the listener starts
Syncthing's Event API is not an infinite permanent event log. The listener may miss events while it is stopped, so it should scan all JSONL histories once at startup.
Active tasks change frequently
The same JSONL file can arrive repeatedly while a conversation is active. Running a complete repair on every event would be wasteful, so a short debounce and a single-execution lock are important.