Fork/side conversation fails when $CODEX_HOME/sessions is a symlink: rollout path 'must be in Codex home directory'

Open 💬 1 comment Opened Aug 19, 2026 by lucasygu

Summary

/fork and side conversations fail for every paginated thread whenever $CODEX_HOME/sessions is a symlink pointing outside the Codex home — a rollout Codex itself wrote is rejected by the fork containment check.

What breaks

When $CODEX_HOME/sessions is a symlink pointing outside the Codex home (a legitimate layout — e.g. session storage relocated to another volume or a shared store), /fork and side conversations fail for every paginated thread with:

thread/fork failed: rollout path `<home>/sessions/2026/08/18/rollout-….jsonl` must be in Codex home directory (code -32600)

The rollout was written by Codex itself, through its own configured home. Reads, resume, archive, and unarchive all work fine in this layout; only fork rejects it.

Why

scoped_rollout_path (thread-store/src/local/helpers.rs) canonicalizes both the root and the rollout path and requires starts_with:

  • For the fork-lineage caller the root is codex_home itself. canonicalize(codex_home) resolves to the real home dir, but canonicalize(rollout_path) resolves through the sessions symlink to the external target — so containment fails for a path the store composed through its own home.
  • Archive/unarchive/delete pass codex_home/sessions (or archived_sessions) as the root, where canonicalizing the root also walks through the symlink — which is why only fork breaks.

Repro needs nothing but ln -s: symlink a home's sessions dir elsewhere, create a session, /fork.

Fix

Accept a rollout path that is lexically contained in the root as written (absolute, no ./.. traversal in the suffix), in addition to canonical containment. The lexical check only ever admits paths expressed through the configured root — exactly the paths the store composes — and the canonicalize-the-rollout step still runs first, so a missing file still errors and the returned path is still fully resolved for downstream reads. A genuinely external rollout path (not under the home as written) is still rejected: the existing read_thread_uses_live_writer_rollout_path_for_external_resume test still passes unchanged.

Test

fork_accepts_rollout_reached_through_symlinked_sessions_dir (unix-gated): a home whose sessions is a symlink to an external dir, a paginated session written through the home path, resumed, then prepare_fork — fails with the error above before this change, passes after.

Patch

External PRs appear closed on this repo (the last non--oai PR predates July), so filing as an issue. A ready-made fix + regression test, rebased on current main and green on cargo test -p codex-thread-store (226/226, the existing external-rollout rejection test unchanged), is here:

https://github.com/lucasygu/codex/commit/0eab6159c4 (branch fix/fork-symlinked-sessions-dir)

Happy for a maintainer to cherry-pick it wholesale or reimplement — the repro test alone (fork_accepts_rollout_reached_through_symlinked_sessions_dir) may be the useful part.

Environment

Reproduced on codex-cli 0.147.0 and 0.148.0, macOS; the check landed with the paginated-fork thread-store work (#35220) and ships in stable ≥ 0.146.1.

View original on GitHub ↗

1 Comment

gappiiii · 2 days ago

I can confirm this on Windows 11 with an NTFS directory junction.

Setup:

C:\Users\<user>\.codex\sessions
    -> D:\CodexSessions

fsutil reparsepoint query reports this as a Windows mount-point reparse point.

For paginated-history threads, /side fails immediately because thread/fork returns:

errorCode=-32600
rollout path `C:\Users\<user>\.codex\sessions\...\rollout-....jsonl`
must be in Codex home directory

The rollout is not missing. The same file is accessible through both the logical junction path and its physical target:

C:\Users\<user>\.codex\sessions\...\rollout-....jsonl
D:\CodexSessions\...\rollout-....jsonl

fsutil file queryfileid returns the same Windows file ID for both paths, confirming that they refer to the same physical file.

I also checked the thread metadata and historical Desktop logs:

17 successful thread/fork calls:
  history_mode = legacy

8 failed thread/fork calls with errorCode -32600:
  history_mode = paginated

The failing paginated threads include newly created threads, so this is not limited to resumed or legacy sessions.

Version observations:

Desktop 26.814.5167.0
app-server 0.148.0-alpha.15
  Successful forks were observed, but only for legacy-history threads.

Desktop 26.818.3698.0
app-server 0.149.0-alpha.4
  Paginated-history forks fail.

Desktop 26.820.7780.0
app-server 0.150.0-alpha.8
  Paginated-history forks still fail.

Because the successful and failing samples use different history modes, I cannot claim a clean version regression boundary. However, the behavior appears to be the same canonical-containment issue described here: the logical rollout path is under $CODEX_HOME/sessions, but canonicalization resolves it through the junction to an external volume, causing the fork containment check to reject a rollout written by Codex itself.