codex - 💡(How to fix) Fix Windows: SkillsWatcher blocks fs::rename in replace_plugin_root_atomically

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Error Message

$root = "$env:TEMP\rename-watcher-repro" $inner = "$root\plugin\1.0.0\skills" $bk = "$env:TEMP\rename-watcher-repro-backup" New-Item -ItemType Directory -Path $inner -Force | Out-Null

$fsw = New-Object System.IO.FileSystemWatcher $inner $fsw.IncludeSubdirectories = $true $fsw.EnableRaisingEvents = $true

try { [System.IO.Directory]::Move("$root\plugin", $bk) "OK: rename succeeded" } catch { "FAIL: $($_.Exception.Message)" }

$fsw.EnableRaisingEvents = $false; $fsw.Dispose() Remove-Item $root,$bk -Recurse -Force -ErrorAction SilentlyContinue

Root Cause

Close the interactive codex window, then re-run codex --version.

Refresh now succeeds because no handle is held.

Code Example

{
  "codexVersion": "0.132.0",
  "checks": {
    "config.load": { "status": "ok" },
    "runtime.provenance": { "details": { "platform": "windows-x86_64", "version": "0.132.0" } }
  }
}

---

$root  = "$env:TEMP\rename-watcher-repro"
$inner = "$root\plugin\1.0.0\skills"
$bk    = "$env:TEMP\rename-watcher-repro-backup"
New-Item -ItemType Directory -Path $inner -Force | Out-Null

$fsw = New-Object System.IO.FileSystemWatcher $inner
$fsw.IncludeSubdirectories = $true
$fsw.EnableRaisingEvents   = $true

try {
    [System.IO.Directory]::Move("$root\plugin", $bk)
    "OK: rename succeeded"
} catch {
    "FAIL: $($_.Exception.Message)"
}

$fsw.EnableRaisingEvents = $false; $fsw.Dispose()
Remove-Item $root,$bk -Recurse -Force -ErrorAction SilentlyContinue

---

FAIL: Exception calling "Move" with "2" argument(s):
"Access to the path 'C:\Users\...\rename-watcher-repro\plugin' is denied."

---

$mp     = "$env:USERPROFILE\repro-mp"
$plugin = "$mp\repro-test-plugin"
New-Item -ItemType Directory -Path "$env:USERPROFILE\.claude-plugin","$plugin\.claude-plugin","$plugin\skills\demo" -Force | Out-Null

@'
{ "name": "repro-mp", "owner": { "name": "repro" },
  "plugins": [ { "name": "repro-test-plugin", "source": "../repro-mp/repro-test-plugin" } ] }
'@ | Set-Content -Encoding UTF8 "$env:USERPROFILE\.claude-plugin\marketplace.json"

@'
{ "name": "repro-test-plugin", "version": "1.0.0" }
'@ | Set-Content -Encoding UTF8 "$plugin\.claude-plugin\plugin.json"

"# Demo`n`nNoop." | Set-Content "$plugin\skills\demo\SKILL.md"

codex plugin add 'repro-test-plugin@repro-mp'

# Start codex interactively in another window and leave it open.

@'
{ "name": "repro-test-plugin", "version": "1.1.0" }
'@ | Set-Content -Encoding UTF8 "$plugin\.claude-plugin\plugin.json"

# In a separate shell, force a refresh.
codex --version
# Expected: cache updates to 1.1.0.
# Actual: refresh fails internally with
#   "failed to move existing plugin root aside: Access is denied. (os error 5)"
# Cache stays at 1.0.0.

# Close the interactive codex window, then re-run `codex --version`.
# Refresh now succeeds because no handle is held.

# Cleanup.
codex plugin remove 'repro-test-plugin@repro-mp'
Remove-Item "$env:USERPROFILE\.claude-plugin","$mp" -Recurse -Force
Remove-Item "$env:USERPROFILE\.codex\plugins\cache\repro-mp" -Recurse -Force -ErrorAction SilentlyContinue
RAW_BUFFERClick to expand / collapse

What version of Codex CLI is running?

codex-cli 0.132.0

What subscription do you have?

ChatGPT Business

Which model were you using?

gpt-5.4 (not model-dependent)

What platform is your computer?

Microsoft Windows NT 10.0.26100.0 x64 (Windows 11 Enterprise 24H2). Windows only; fs::rename is not blocked by a watching process on Linux or macOS.

What terminal emulator and version are you using (if applicable)?

Windows Terminal (PowerShell 7).

Codex doctor report

{
  "codexVersion": "0.132.0",
  "checks": {
    "config.load": { "status": "ok" },
    "runtime.provenance": { "details": { "platform": "windows-x86_64", "version": "0.132.0" } }
  }
}

What issue are you seeing?

On Windows, in-session plugin cache updates can fail with Access is denied. (os error 5) while the originating codex session is alive, and the failure recurs across restart cycles.

The rename call that fails is in replace_plugin_root_atomically, specifically the fs::rename(target_root, &backup_root) that moves the existing cache plugin dir aside before staging the new version. The handle that blocks it belongs to SkillsWatcher, which constructs a FileWatcher wrapping a notify::RecommendedWatcher and keeps it open for the lifetime of the session.

On Windows, that watcher opens the watched directory via CreateFileW with dwDesiredAccess = FILE_LIST_DIRECTORY (access right) and dwShareMode = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE (share-mode flags), then registers change notifications on that handle via ReadDirectoryChangesW.

FILE_SHARE_DELETE is what lets external DeleteFileW / RemoveDirectoryW calls (e.g. rd /s /q) succeed against the watched path while the watcher is alive. Windows does not extend that allowance to renames: MoveFileExW (which std::fs::rename calls on Windows when the source is a directory) returns ERROR_ACCESS_DENIED if any open handle exists on the path being renamed or on any directory beneath it, regardless of share mode. A SkillsWatcher handle rooted at target_root/<version>/skills/ is therefore enough to block fs::rename(target_root, ...) for the lifetime of the session.

A consequence worth flagging: the "close codex, let the next launch refresh the cache" recovery does not work. The app-server starts SkillsWatcher on launch and reattaches to the still-stale cache before any subsequent update attempt, so the same MoveFileExW failure recurs. The only recovery from outside the process is to shut down all codex processes, wipe the cache plugin dir, and start codex again; on that fresh start replace_plugin_root_atomically takes the else branch (no backup-swap needed) and installs cleanly.

What steps can reproduce the bug?

Two reproductions below. The first is a primitive-level repro that does not require running codex and isolates the rename failure to the MoveFileExW + ReadDirectoryChangesW interaction. The second is end-to-end through codex. Both run on codex-cli 0.132.0, Windows 11 24H2.

A. Primitive repro (no codex required)

.NET's FileSystemWatcher opens the directory with the same FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE mode that the notify crate uses, so it is a faithful stand-in. Move-Item in PowerShell has retry and copy-then-delete fallback paths that can mask the issue; [System.IO.Directory]::Move calls MoveFileExW directly, matching std::fs::rename on Windows.

$root  = "$env:TEMP\rename-watcher-repro"
$inner = "$root\plugin\1.0.0\skills"
$bk    = "$env:TEMP\rename-watcher-repro-backup"
New-Item -ItemType Directory -Path $inner -Force | Out-Null

$fsw = New-Object System.IO.FileSystemWatcher $inner
$fsw.IncludeSubdirectories = $true
$fsw.EnableRaisingEvents   = $true

try {
    [System.IO.Directory]::Move("$root\plugin", $bk)
    "OK: rename succeeded"
} catch {
    "FAIL: $($_.Exception.Message)"
}

$fsw.EnableRaisingEvents = $false; $fsw.Dispose()
Remove-Item $root,$bk -Recurse -Force -ErrorAction SilentlyContinue

Observed on this machine:

FAIL: Exception calling "Move" with "2" argument(s):
"Access to the path 'C:\Users\...\rename-watcher-repro\plugin' is denied."

With the same setup but no FileSystemWatcher attached, the same Directory.Move succeeds. The watcher's open handle on a grandchild of the rename source is enough to deny it. That matches exactly what replace_plugin_root_atomically does while SkillsWatcher is alive.

B. End-to-end repro through codex

Set up a marketplace under $HOME, install with a skills/ dir present, then bump the version while a session is live.

$mp     = "$env:USERPROFILE\repro-mp"
$plugin = "$mp\repro-test-plugin"
New-Item -ItemType Directory -Path "$env:USERPROFILE\.claude-plugin","$plugin\.claude-plugin","$plugin\skills\demo" -Force | Out-Null

@'
{ "name": "repro-mp", "owner": { "name": "repro" },
  "plugins": [ { "name": "repro-test-plugin", "source": "../repro-mp/repro-test-plugin" } ] }
'@ | Set-Content -Encoding UTF8 "$env:USERPROFILE\.claude-plugin\marketplace.json"

@'
{ "name": "repro-test-plugin", "version": "1.0.0" }
'@ | Set-Content -Encoding UTF8 "$plugin\.claude-plugin\plugin.json"

"# Demo`n`nNoop." | Set-Content "$plugin\skills\demo\SKILL.md"

codex plugin add 'repro-test-plugin@repro-mp'

# Start codex interactively in another window and leave it open.

@'
{ "name": "repro-test-plugin", "version": "1.1.0" }
'@ | Set-Content -Encoding UTF8 "$plugin\.claude-plugin\plugin.json"

# In a separate shell, force a refresh.
codex --version
# Expected: cache updates to 1.1.0.
# Actual: refresh fails internally with
#   "failed to move existing plugin root aside: Access is denied. (os error 5)"
# Cache stays at 1.0.0.

# Close the interactive codex window, then re-run `codex --version`.
# Refresh now succeeds because no handle is held.

# Cleanup.
codex plugin remove 'repro-test-plugin@repro-mp'
Remove-Item "$env:USERPROFILE\.claude-plugin","$mp" -Recurse -Force
Remove-Item "$env:USERPROFILE\.codex\plugins\cache\repro-mp" -Recurse -Force -ErrorAction SilentlyContinue

procmon filtered on Operation = SetRenameInformationFile, Path contains \.codex\plugins\cache\, Result = ACCESS DENIED shows the failing call originating from the codex process while the same process has a CreateFile on the corresponding skills\ dir with Desired Access: Read Data/List Directory, Synchronize and Share Mode: Read, Write, Delete.

What is the expected behavior?

Codex should be able to refresh the plugin cache to a new plugin version on Windows, including while a session is running.

Additional information

None.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

codex - 💡(How to fix) Fix Windows: SkillsWatcher blocks fs::rename in replace_plugin_root_atomically