Codex App branch switch uses a different worktree than the task terminal

Open 💬 3 comments Opened Aug 8, 2026 by funct7

Summary

The Codex macOS app can appear to use different Git worktree contexts for the terminal and branch-switch UI within the same task.

Reproduction

  1. Start a Codex task from a Git repository using a managed worktree.
  2. In the task terminal, run:

``
pwd
git branch --show-current
`
The terminal reports a managed worktree path such as
/Users/<user>/.codex/worktrees/7e31/WantPic and branch ft/blog_image`.

  1. In the Codex app's branch switcher, select ft/blog_image.
  2. The app reports:

``
fatal: 'ft/blog_image' is already used by worktree at '/Users/<user>/.codex/worktrees/WantPic'
``

  1. git worktree list shows the branch is already attached to the managed task worktree, while the main checkout remains on another branch.
  2. Restarting the ChatGPT/Codex app initially restores the expected branch, but resuming/reconnecting to this task causes the wrong branch context to return.

Expected behavior

The branch controls and task terminal should resolve the same repository/worktree context. Resuming a task should preserve its associated worktree and branch.

Actual behavior

The task environment can alternate between the managed feature branch and the main branch. The branch-switch operation appears to resolve a different or stale worktree path and rejects the feature branch as already in use.

Screenshot evidence

The supplied screenshots show the following sequence:

  • Screenshot 2026-08-08 at 10.54.19 PM.png: the Environment panel shows 1.10.0/master.
  • Screenshot 2026-08-08 at 10.54.47 PM.png: it shows ft/blog_image, the commit [1.10.0] 블로그 썸네일 추가, and one comment.
  • Screenshot 2026-08-08 at 10.55.05 PM.png: it has returned to 1.10.0/master.

This alternating state occurs while working with the same task and is consistent with the task being rebound to the wrong worktree after resume/reconnect.

Additional context (hypothesis)

The problem did not occur when the worktree and task were first created. It may correlate with reconnecting to the repository remotely and/or reopening the task from a remote session. This is a suspicion rather than a confirmed cause, but it may help narrow down when the repository/worktree context becomes stale or inconsistent.

Environment

  • ChatGPT macOS app (Version 26.803.41515)
  • macOS (26.5.2)
  • Git worktrees enabled

<img width="314" height="498" alt="Image" src="https://github.com/user-attachments/assets/d2809e03-972c-4f08-aac6-8cc6908fdfaf" />
<img width="321" height="370" alt="Image" src="https://github.com/user-attachments/assets/9b6ffd3c-468f-4aea-a5f9-f3cc5de638a1" />
<img width="316" height="400" alt="Image" src="https://github.com/user-attachments/assets/6e38f4d2-e0c9-4b4b-a4a4-62952c78bda7" />

View original on GitHub ↗

3 Comments

cztamas · 18 days ago

The exact same issue is happening for me. When I create a new chat with worktrees enabled, a new worktree gets created and the agent operates in it. But the UI widget shows the default worktree's state, and the "Create and checkout new branch" action in the widget operates on the default worktree instead of the actually active one. It started happening after the latest update.

Codex version 26.803.41515, macOS Tahoe 26.5

funct7 · 17 days ago

Not sure about the bug, but this is how I "fixed" the issue.
The section below was written by ChatGPT.

---

The task terminal was correctly running in a managed worktree on the feature branch, but the Environment/branch UI resolved the saved repository checkout instead. In my case, the task metadata and terminal pointed to the OCR worktree and codex/ocr/main, while the UI showed the main checkout and 1.15.2/master. The worktree itself was registered correctly in git worktree list.

What fixed it was re-binding the task through the handoff workflow:

  1. Hand the task off to the local checkout.
  2. Hand the resulting task back to Local again, which makes Codex reuse the existing managed worktree and re-associate the feature branch.
  3. Verify that the task metadata, terminal, and UI all point to the same worktree.

I did not delete or recreate the worktree, and I did not manually move the branch. The handoff preserved the existing uncommitted change. This suggests a stale task-to-worktree association after resume/reconnect; I cannot confirm the underlying cause, but forcing the local-checkout -> managed-worktree rebind cleared it.

cztamas · 15 days ago

This is what Codex came up with when I sent it to investigate:

Status: DONE_WITH_CONCERNS — the release and faulty path are high-confidence; the desktop UI’s original TypeScript source is not public, so I cannot name its internal source commit.

Conclusion

The regression was introduced in Codex desktop 26.803.41515, published August 7, 2026. The immediately preceding production release, 26.730.61639, does not contain the triggering behavior.

The bug is in the desktop renderer’s new “follow an existing thread owner” resume flow—not Git and probably not the open-source Rust app-server.

| Desktop release | Embedded Codex | Relevant behavior |
|---|---:|---|
| 26.730.61639 | 0.147.0-alpha.1.2 | Normal thread hydration |
| 26.803.41515 | 0.147.0-alpha.6.5 | Adds followExistingOwner: true and the faulty early return |
| 26.803.61601 | 0.147.0-alpha.6.5 | Still contains the bug |

This matches the symptoms and version reported in [issue #37591](https://github.com/openai/codex/issues/37591).

Actual cause

The new release changed automatic task resume to send:

maybeResumeConversation({
  // ...
  followExistingOwner: true,
});

When another renderer already owns the task, the resume manager now:

const owner = await findThreadOwner(...);

setConversationStreamRole(threadId, {
  role: "follower",
  ownerClientId: owner,
});

conversation.resumeState = "resumed";
return;

That return happens before the normal thread/read, thread/resume, CWD reconciliation, environment hydration, and workspace-state synchronization.

Consequently:

  • The existing owner and task terminal remain attached to the correct managed worktree.
  • The newly opened follower renderer retains the CWD loaded from its local thread catalog or default project assignment.
  • The branch/environment widget uses that stale renderer state.
  • Git commands therefore run against the default or an older worktree and report that the branch is already checked out elsewhere.

The exact new call is visible in the extracted current bundle at [/private/tmp/use-resume.js](/private/tmp/use-resume.js). The preceding release’s equivalent bundle, [/private/tmp/use-resume-prev.js](/private/tmp/use-resume-prev.js), has no followExistingOwner field or owner-discovery shortcut. The early-return implementation is readable in [/private/tmp/chatgpt-app-initial.pretty.js:160660](/private/tmp/chatgpt-app-initial.pretty.js:160660).

That explains all the distinctive observations:

  • terminal and branch switcher disagree;
  • restarting briefly repairs the UI;
  • reconnecting makes it wrong again;
  • a local-to-local handoff repairs the association.

A second, latent CWD bug

Both releases also contain this approximate resolver:

const persisted = responseCwd || threadCwd;

return requestedCwd &&
  persisted &&
  isEqualOrDescendant(requestedCwd, persisted)
    ? requestedCwd
    : persisted || requestedCwd || fallbackCwd;

For sibling managed worktrees such as:

requested: ~/.codex/worktrees/7e31/WantPic
stored:    ~/.codex/worktrees/WantPic

the requested directory is not a descendant of the stored directory, so the stale stored directory wins. I reproduced that result directly.

This logic predates 26.803.41515, so it did not create the release boundary, but it can reinforce the same failure during a full resume.

There is also a relevant newer open-source change, [#37198](https://github.com/openai/codex/commit/547080e4d690cdeea12f427a8d9c5165928821ed), which explicitly allows ThreadResumeResponse.thread.cwd to be a persisted CWD while the top-level cwd is the live resumed CWD. That commit was first included in 0.148.0-alpha.1 and was not in the affected 26.803.41515 build, so it is not the original cause. It does, however, make it especially important that the desktop stops treating those two fields as interchangeable. The dual fields are visible in [thread.rs](/Users/cztamas/dev/tools/codex/codex-rs/app-server-protocol/src/protocol/v2/thread.rs:405), and the live response CWD is populated in [thread_processor.rs](/Users/cztamas/dev/tools/codex/codex-rs/app-server/src/request_processors/thread_processor.rs:3462).

Suggested fix

The primary fix belongs in the desktop follower-adoption flow:

  1. After discovering an existing owner, request and install the owner’s complete task snapshot before marking the follower resumed.
  2. That snapshot must include the canonical active CWD, applied worktree assignment, environments, Git metadata, and runtime workspace roots.
  3. Only then set the renderer to follower and return.
  4. Branch actions must resolve their Git root from that canonical active CWD—the same value used by the terminal—not from the catalog/default project.

A safe short-term hotfix is to remove followExistingOwner: true until follower workspace synchronization is implemented.

The CWD resolver should also be separated by phase:

// Before resume: an explicit task/worktree selection wins.
const resumeCwd =
  requestedCwd ??
  persistedThreadCwd ??
  fallbackCwd;

// After resume: the app-server's live response is authoritative.
const activeCwd =
  responseCwd ??
  requestedCwd ??
  persistedThreadCwd ??
  fallbackCwd;

thread.cwd should be treated as persisted metadata, not an equivalent alternative to the top-level live response.cwd.

The regression test should create two sibling worktrees, seed stale catalog/thread metadata, open a second renderer as a follower, and assert that terminal CWD, environment CWD, Git root, and branch-switch command CWD all remain the managed task worktree after repeated reconnects.

No repository files were changed. One limitation is structural: OpenAI documents the CLI and app-server as open-source components, but the full desktop renderer is not part of the repository, so the desktop finding came from comparing the signed application bundles rather than source history. [OpenAI’s component list](https://learn.chatgpt.com/docs/open-source) confirms the public repository boundary.