[App/Windows/WSL] git-init-watcher retries permanent EISDIR on workspace root every five seconds

Open 💬 5 comments Opened Jul 13, 2026 by loraldx
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Summary

Codex Desktop 26.707.8168.0 on Windows 11 repeatedly retries a Windows-side file watcher after receiving EISDIR for a WSL2 workspace root. The retry interval eventually settles at approximately five seconds and continues while the application remains open.

Related to #32183, but this reproduces without using the in-app browser and appears to involve the workspace-level git-init-watcher rather than only the browser-related git-repo-watcher path.

Sanitized observation

Workspace form:

\wsl$<distro>home<user><workspace>

Observed behavior:

  • Windows Node fs.watch against the WSL workspace root returns EISDIR.
  • Watching the nested Git repository root produces the same result.
  • The behavior also reproduces through the equivalent \wsl.localhost path and a mapped drive.
  • The git-init-watcher retries after failure, with exponential delay capped at approximately five seconds.
  • One application run recorded 121 failures in about ten minutes.
  • Historical application logs contained more than 17,000 failures from the same error family.
  • Git and repository operations performed inside WSL itself continue to work normally.

User names, repository names, task identifiers, full paths, and raw logs have been omitted.

Steps to reproduce

  1. On Windows 11 with WSL2, open a Git workspace located under the Linux home directory in Codex Desktop.
  2. Use a workspace path exposed through \wsl$ or \wsl.localhost.
  3. Leave the desktop application open.
  4. Inspect the Codex application logs for git-init-watcher or file-watch failures.

Actual behavior

A permanent EISDIR watcher failure enters an effectively unbounded retry loop, producing repeated work and log churn approximately every five seconds.

Expected behavior

  • Detect unsupported or permanent watcher errors such as EISDIR.
  • Fall back to bounded polling when appropriate, or disable that watcher after a small number of failures.
  • Avoid retrying permanent watcher failures indefinitely.
  • Prefer a WSL-native watcher when the workspace is handled by the WSL execution backend.

Environment

  • Codex Desktop: 26.707.8168.0
  • OS: Windows 11 x64
  • WSL2: Ubuntu
  • Workspace stored in the Linux filesystem under the WSL home directory
  • Reproduces without invoking the in-app browser

View original on GitHub ↗

5 Comments

github-actions[bot] contributor · 1 month ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #32183

Powered by Codex Action

gilmarvitor · 1 month ago

Adding another independent reproduction of this same behavior.

Environment:

  • Codex Desktop/MSIX: OpenAI.Codex 26.707.9981.0
  • - Runtime reported in logs: 26.707.72221
  • - - Codex CLI: 0.144.2
  • - - - Windows 11, build 10.0.26200.8875, x64
  • - - - - WSL 2.7.10, Ubuntu 24.04, kernel 6.18.33.2
  • - - - - - Agent configured to run in WSL2; workspace on ext4 under /home, with no Git repository initialized

Observed behavior:

  • The Windows-side process attempts to watch a path shaped like \\wsl$\Ubuntu\home\<wsl-user>\workspace\<workspace>.
  • - fs.watch repeatedly fails with EISDIR.
  • - - The retry occurs approximately every 5 seconds, at roughly 12 warnings per minute — consistent with the ~121 failures in ~10 minutes already reported in this issue.

I don't consider the following to be fixes, only workarounds: running git init just to silence the watcher; keeping an artificial Windows project open; moving projects to Windows; or switching the agent to native Windows mode.

This reinforces the suggestion already noted here: detect the permanent error and apply backoff/disable the watcher, or run the watcher in the same environment as the filesystem (WSL), instead of retrying EISDIR indefinitely. I don't have confirmation this is the root cause — this is only another reproduction of the symptom.

stephanelepain-hub · 11 days ago

Same defect reproduces on Linux with a different errno — ENOENT instead of EISDIR — which suggests the retry logic, not the platform or the specific error, is the problem.

Environment

| | |
|---|---|
| App | Codex Desktop for Linux, chatgpt-26.810.52044-1.x86_64 (official RPM) |
| OS | Fedora 44, GNOME / Wayland, x86_64 |
| Kernel | 7.1.8 |
| Workspace | plain local ext4 path — no WSL, no SMB, no mapped drive |

Observation

The recorded workspace root simply does not exist on this host (it belongs to a different machine). fs.watch() returns ENOENT, and git-init-watcher retries indefinitely:

[git-init-watcher] Failed to watch workspace root for git init
    cwd=/home/<user>/git/<project> errorCode=ENOENT
    errorMessage="ENOENT: no such file or directory, watch '/home/<user>/git/<project>'"
    at e.evaluateImpl (/usr/lib/chatgpt/resources/app.asar/.vite/build/worker.js:200:24538)
    at e.evaluate     (/usr/lib/chatgpt/resources/app.asar/.vite/build/worker.js:200:23689)
    at e1.readLoop    (/usr/lib/chatgpt/resources/app.asar/.vite/build/worker.js:200:37425)

Measured 55 occurrences in 30 minutes (~one every 5s), matching the cap described in the original report. A second independent 1-hour sample showed the same line still dominating the journal. No crash, no coredump — the app is otherwise usable.

Why this may be worth generalising

For this particular stale path, ENOENT never resolved — the directory stayed absent for the life of the process, so retrying could not succeed. ENOENT is not permanent in general (a directory may be created later), so the useful distinction is probably "has not resolved after N attempts" rather than "permanent by errno". Related reports show repeated Git-watcher failures in other environments, though not necessarily in the same watcher or code path: #33764, #29084, and #36961. My Linux observation is specifically git-init-watcher returning ENOENT. A fix that classifies non-transient fs.watch() errors and stops retrying — rather than special-casing WSL or SMB paths — would cover all of them.

On a laptop the cost is a wakeup every 5s for the lifetime of the process, plus journal spam that buries other diagnostics.

stephanelepain-hub · 11 days ago
Superseded — see my later comment. The config.toml entry below turned out not to be the source: removing it did not stop the loop. The path is restored from persisted session state. The rest of this comment is kept for the record.

Correction to my earlier comment — I said I couldn't find where the stale workspace root came from and guessed at synced account state. That was wrong: I searched the wrong directory. It is plain local config.

The path is a project entry in ~/.codex/config.toml:

[projects."/home/<user>/git/<project>"]
trust_level = "trusted"

That directory does not exist on this host, and git-init-watcher retries fs.watch() against it every ~5s for the life of the process.

Local reproduction (no account state, no WSL/SMB needed)

  1. Quit the app.
  2. Add a [projects."<absolute path that does not exist>"] entry to ~/.codex/config.toml (or add a workspace, then delete the directory).
  3. Start the app and leave it idle.
  4. journalctl --user -f | grep git-init-watcher

The ENOENT line repeats indefinitely, no backoff, no give-up.

Superseded — see my later comment. The config.toml entry turned out not to be the source: removing it did not stop the loop. The path is restored from persisted session state. I have only tested Linux, so I can't say how this behaves on macOS or Windows — which fits this being one watcher bug across platforms (ENOENT here, EISDIR on WSL in the original report) rather than several platform-specific ones.

stephanelepain-hub · 11 days ago

Trigger isolated on Linux: the stale path survived every configuration-level removal I could find.

Same host as my earlier comments (Fedora 44, chatgpt-26.810.52044-1.x86_64, Wayland). Following the ENOENT loop back, the missing workspace root kept returning after every config-level removal. The evidence points at historical rollout files as the source, though I have not read the code and cannot confirm the mechanism.

Chain of custody

Three rollout files dated 2026-05-01:

~/.codex/sessions/2026/05/01/rollout-2026-05-01T14-14-03-<uuid>.jsonl

each containing:

"cwd":"/home/<user>/git/<project>"

That directory no longer exists. After a restart the rows were observed recreated in local_thread_catalog; state_5.sqlite was separately observed to hold the same path, though I did not watch it being repopulated. The two stores were local_thread_catalog in ~/.codex/sqlite/codex-dev.db, and threads in ~/.codex/state_5.sqlite. The thread IDs in local_thread_catalog match the rollout filenames exactly; I did not perform the same check against state_5.sqlite.

Why config-level fixes do not work

Verified in this order, measuring after each step:

  1. Removed the [projects."…"] entry from config.toml → thread persisted.
  2. Launched with cwd /tmp and zero [projects] entries → thread persisted.
  3. Deleted the rows from local_thread_catalog → rows were recreated on next start, and the thread came back heavier than before.
  4. Moved the three rollout files aside and cleared both derived tables → thread went idle and stayed idle across a restart.

Only step 4 held — but note it changed three things at once (moved the rollouts and cleared both stores), so it does not isolate which of them was authoritative. What it does establish is that no configuration-level change I tried was sufficient; clearing persisted state was required.

Measured cost (instantaneous /proc sampling, not ps lifetime averages)

| state | app CPU | RSS | Tctl |
|---|---|---|---|
| as installed, settled idle | 30.0% | 3417 MB | 64.9 °C |
| as installed, peak observed | 41.8% | 3437 MB | 74.8 °C |
| after moving rollouts aside and clearing both stores | 2.0% | 2328 MB | 58.0 °C |

A thread named git inside the main process accounted for 99 of 121 ticks (82% of the main process's work) in one sample, and 242 ticks in a later one. git-init-watcher log lines went from ~55 per 30 min to 0. Across the full mitigation sequence, settled Tctl fell from 64.9 °C to 58.0 °C; the pre-change peak was 74.8 °C. These measurements do not isolate the temperature contribution of each change.

Observed effects associated with the same stale path

  • The behaviour is consistent with there being no staleness check when the thread index is rebuilt — inferred from observation, not from reading the code. Either way, a cwd that no longer exists should be skipped rather than watched. (I measured CPU in a thread named git and the watcher's log output; I did not instrument what it was reading, so "scanning" would be my assumption rather than a measurement.)
  • The retry loop already described in this issue is the visible symptom; the CPU burn is the larger cost and is easy to miss because the app remains responsive.

Unrelated but found alongside — the Linux app launches its GPU process with --ozone-platform=x11 on a Wayland session, Under X11, both GetVSyncParametersIfAvailable() failed and Unable to get gpu adapter appeared in the journal and the GPU process measured 17.7% CPU on an idle window. Under Wayland, it measured 6.5%, and both error classes were absent after restart. Happy to open that separately if it is out of scope here.

---

Edited after posting for accuracy. Two changes worth flagging: (a) the measurement table originally showed a single "as installed" row of 41.8% / 74.8 °C, which was the peak sample rather than the settled idle baseline and overstated the improvement — both pre-cleanup samples are now shown separately; (b) several statements about the app's internals were phrased as fact when they are inferred from observed behaviour, and have been reworded. I have not read the source. The measurements themselves are unchanged.

Further edit: narrowed several statements that outran the evidence — which store was observed being recreated versus merely holding the path, which table had its IDs cross-checked, "scanning" (not instrumented), "free-running" (an inference), and the fact that the final step was a compound intervention rather than an isolated one.