Summary
On codex-cli 0.159.2 (Windows 11 x64, build 26220) the managed app-server daemon install fails with a bare os error 5, and evidence points at a different failing call than the one identified in #48853. Here, the junction conversion in select_release succeeds; the install dies earlier, at std::fs::rename(stage, release). Flagging this because the fix proposed in #48853 (fall back when FSCTL_SET_REPARSE_POINT is denied) would never be reached on this machine.
Symptom:
Installing daemon from CLI version 0.159.2 into C:\Users\linzh\.codex\packages\app-server-daemon...
Error: 拒绝访问。 (os error 5)
and from the TUI:
Error: 拒绝访问。 (os error 5)
To work without the background server, rerun the same command with --no-daemon (including resume or fork and its arguments).
(The localized text is just this machine's locale; it is the same ERROR_ACCESS_DENIED.)
The staged package is complete, but releases/ stays empty
Polling packages\app-server-daemon\releases every 60 ms while codex app-server daemon start runs, the last snapshot before the process exits is:
last staging snapshot : 45 files / 427.6MB
releases/ : (empty)
45 files / 427.6 MiB is the entire vendor tree (bin\codex.exe 309.7 MiB, bin\codex-code-mode-host.exe 71.2 MiB, codex-path\rg.exe, codex-resources\..., codex-package.json). So in prepare_install.rs everything up to and including validate_package(stage) succeeds, and package_tree writes every file.
The staging directory is a tempfile::TempDir and is removed on drop. If std::fs::rename(stage.path(), &release) had succeeded, releases\0.159.2-x86_64-pc-windows-msvc would still be there afterwards. It is not — releases/ is empty apart from unrelated leftovers.
The junction path works fine here
I pre-staged a valid release directory so that release.try_exists()? at prepare_install.rs:254 is true, which makes the installer take the warm path and skip the rename entirely:
$vendor = "<npm global>\@openai\codex\node_modules\@openai\codex-win32-x64\vendor\x86_64-pc-windows-msvc"
$rel = "$env:USERPROFILE\.codex\packages\app-server-daemon\releases\0.159.2-x86_64-pc-windows-msvc"
New-Item -ItemType Directory -Force -Path $rel
Copy-Item (Join-Path $vendor '*') $rel -Recurse -Force # full 45-file tree
codex app-server daemon start
{"status":"started","backend":"pid","pid":42808,
"managedCodexPath":"C:\\Users\\linzh\\.codex\\packages\\app-server-daemon\\current\\bin\\codex.exe",
"managedCodexVersion":"0.159.2","cliVersion":"0.159.2","appServerVersion":"0.159.2"}
current did not exist beforehand, so select_release took its cold path — temp dir, create_dir(current), retarget_junction (DeviceIoControl(FSCTL_SET_REPARSE_POINT)), rename — and the result is a working junction:
Get-Item "$env:USERPROFILE\.codex\packages\app-server-daemon\current" -Force
# Mode Name LinkType
# l---- current Junction -> ...\releases\0.159.2-x86_64-pc-windows-msvc
auto-update-version was written at the same timestamp. So on this machine FSCTL_SET_REPARSE_POINT succeeds and the daemon starts and runs normally. The failing call is upstream of it.
Where I think it actually fails
// codex-rs/app-server-daemon/src/prepare_install.rs (tag rust-v0.159.2)
// 229 let digest = package_tree(source, Some(stage.path()))?; // OK - all 45 files written
// 230 validate_package(stage.path())?; // OK
// 236 anyhow::ensure!(package_tree(source, None)? == digest // OK
// && std::fs::read(stage.path().join("codex-package.json"))? == manifest_bytes
// && managed_install::executable_identity(&staged_exe).await? == running_identity, ...)
// 254 if release.try_exists()? { ... } else {
// 265 std::fs::rename(stage.path(), &release)?; // <-- ERROR_ACCESS_DENIED
The error is a bare Error: 拒绝访问。 (os error 5) with no Caused by: chain, ~2 s after the Installing daemon from CLI version 0.159.2 into ... line. That detail matters, because the neighbouring candidates are distinguishable by whether they carry context:
install_lock.rs treats PermissionDenied as "lock busy" and retries for 30 s, then fails with timed out waiting for daemon installer lock ... — not what we see, so the install lock is fine.
executable_identity() wraps errors in failed to read executable ...; managed_codex_version() wraps them in failed to invoke managed Codex binary .... Neither text appears, and no codex.exe --version child process is ever spawned, so execution stops before both.
package_tree(root, None) and std::fs::read(stage/codex-package.json) are the only bare ? left in that block. A faithful Node replication of package_tree(root, None) — same DFS order (Vec stack, reverse-sorted readdir), realpath + lstat + full read of every file — completes over all 45 files with no error. So the source-side re-read does not look like the culprit.
That leaves std::fs::rename(stage, release) at line 265. MoveFileEx on a directory fails with ERROR_ACCESS_DENIED when a file inside the tree is open without FILE_SHARE_DELETE, and this tree just received a 309.7 MiB codex.exe and a 71.2 MiB codex-code-mode-host.exe. Something holding either briefly after the write — a scanner, or a codex handle — would produce exactly this. This is a hypothesis I could not prove directly; I was not able to capture the failing CreateFile/MoveFileEx.
If this holds, a MoveFileEx retry (or staging outside the same volume / renaming a bare directory before populating it) would fix the case that #48853's junction fallback does not.
Eliminations, for the record
| Hypothesis |
Evidence against |
ACLs on .codex |
CODEX_HOME pointed at a brand-new C:\codexhome and at a brand-new %TEMP% dir: identical failure. Zero DENY ACEs anywhere in the vendor tree. The several hundred S-1-5-21-<other-machine>-... DENY ACEs on .codex come from cloned profiles, and none of those SIDs are in the user token (whoami /groups lists only WaterRun\docker-users). |
| Controlled Folder Access |
Get-MpPreference.EnableControlledFolderAccess = 0 (disabled). |
| Antivirus |
Real-time protection is enabled here (unlike the report in #48853), and the Defender Operational log has no matching events since 09:20. |
| One specific file in the package |
The real 8.2 MiB codex-command-runner.exe alone, and the real 17.7 MiB codex-windows-sandbox-setup.exe alone, both stage successfully. Shrinking both to 1 MiB dummies still fails. Removing all of codex-resources changes the error to local Codex package is missing codex-resources/codex-command-runner.exe, proving package_tree completes. |
| Working directory |
Reproduced from D:\工作\项目\yy-subsystems\systems\infra\yy-agent-hub, from %TEMP%, and with a fresh home on C:\. |
Impact and verification
codex exec is unaffected — it does not need the daemon. With the real ~/.codex it completed and returned PONG (exit 0, 35.7 s, model gpt-6.1-sol).
- With the release pre-staged,
codex --yolo launched in the project directory stayed alive (20 s, no exit) and created a new session rollout (~10 MB). Before the workaround it exited in under a second with os error 5.
codex app-server daemon version then reports {"status":"running",...,"appServerVersion":"0.159.2"}.
This also independently confirms @shgef's correction in #48853: the release directory has to mirror the whole vendor tree, not just bin\codex.exe.
Reproduction
codex app-server daemon start # -> Error: 拒绝访问。 (os error 5), exit 1, ~2s
codex app-server daemon start reproduces without a tty, so it is easier to automate than the TUI path.
Relationship to other reports
Environment
- Windows 11 Pro x64, 10.0.26220
- codex-cli 0.159.2, npm global install
- Non-elevated PowerShell
- Defender real-time protection on, Controlled Folder Access off
HTTP_PROXY/HTTPS_PROXY = http://127.0.0.1:7890
Summary
On codex-cli 0.159.2 (Windows 11 x64, build 26220) the managed app-server daemon install fails with a bare
os error 5, and evidence points at a different failing call than the one identified in #48853. Here, the junction conversion inselect_releasesucceeds; the install dies earlier, atstd::fs::rename(stage, release). Flagging this because the fix proposed in #48853 (fall back whenFSCTL_SET_REPARSE_POINTis denied) would never be reached on this machine.Symptom:
and from the TUI:
(The localized text is just this machine's locale; it is the same
ERROR_ACCESS_DENIED.)The staged package is complete, but
releases/stays emptyPolling
packages\app-server-daemon\releasesevery 60 ms whilecodex app-server daemon startruns, the last snapshot before the process exits is:45 files / 427.6 MiB is the entire vendor tree (
bin\codex.exe309.7 MiB,bin\codex-code-mode-host.exe71.2 MiB,codex-path\rg.exe,codex-resources\...,codex-package.json). So inprepare_install.rseverything up to and includingvalidate_package(stage)succeeds, andpackage_treewrites every file.The staging directory is a
tempfile::TempDirand is removed on drop. Ifstd::fs::rename(stage.path(), &release)had succeeded,releases\0.159.2-x86_64-pc-windows-msvcwould still be there afterwards. It is not —releases/is empty apart from unrelated leftovers.The junction path works fine here
I pre-staged a valid release directory so that
release.try_exists()?at prepare_install.rs:254 is true, which makes the installer take the warm path and skip the rename entirely:{"status":"started","backend":"pid","pid":42808, "managedCodexPath":"C:\\Users\\linzh\\.codex\\packages\\app-server-daemon\\current\\bin\\codex.exe", "managedCodexVersion":"0.159.2","cliVersion":"0.159.2","appServerVersion":"0.159.2"}currentdid not exist beforehand, soselect_releasetook its cold path — temp dir,create_dir(current),retarget_junction(DeviceIoControl(FSCTL_SET_REPARSE_POINT)),rename— and the result is a working junction:auto-update-versionwas written at the same timestamp. So on this machineFSCTL_SET_REPARSE_POINTsucceeds and the daemon starts and runs normally. The failing call is upstream of it.Where I think it actually fails
The error is a bare
Error: 拒绝访问。 (os error 5)with noCaused by:chain, ~2 s after theInstalling daemon from CLI version 0.159.2 into ...line. That detail matters, because the neighbouring candidates are distinguishable by whether they carry context:install_lock.rstreatsPermissionDeniedas "lock busy" and retries for 30 s, then fails withtimed out waiting for daemon installer lock ...— not what we see, so the install lock is fine.executable_identity()wraps errors infailed to read executable ...;managed_codex_version()wraps them infailed to invoke managed Codex binary .... Neither text appears, and nocodex.exe --versionchild process is ever spawned, so execution stops before both.package_tree(root, None)andstd::fs::read(stage/codex-package.json)are the only bare?left in that block. A faithful Node replication ofpackage_tree(root, None)— same DFS order (Vecstack, reverse-sortedreaddir),realpath+lstat+ full read of every file — completes over all 45 files with no error. So the source-side re-read does not look like the culprit.That leaves
std::fs::rename(stage, release)at line 265.MoveFileExon a directory fails withERROR_ACCESS_DENIEDwhen a file inside the tree is open withoutFILE_SHARE_DELETE, and this tree just received a 309.7 MiBcodex.exeand a 71.2 MiBcodex-code-mode-host.exe. Something holding either briefly after the write — a scanner, or a codex handle — would produce exactly this. This is a hypothesis I could not prove directly; I was not able to capture the failingCreateFile/MoveFileEx.If this holds, a
MoveFileExretry (or staging outside the same volume / renaming a bare directory before populating it) would fix the case that #48853's junction fallback does not.Eliminations, for the record
.codexCODEX_HOMEpointed at a brand-newC:\codexhomeand at a brand-new%TEMP%dir: identical failure. Zero DENY ACEs anywhere in the vendor tree. The several hundredS-1-5-21-<other-machine>-...DENY ACEs on.codexcome from cloned profiles, and none of those SIDs are in the user token (whoami /groupslists onlyWaterRun\docker-users).Get-MpPreference.EnableControlledFolderAccess= 0 (disabled).codex-command-runner.exealone, and the real 17.7 MiBcodex-windows-sandbox-setup.exealone, both stage successfully. Shrinking both to 1 MiB dummies still fails. Removing all ofcodex-resourceschanges the error tolocal Codex package is missing codex-resources/codex-command-runner.exe, provingpackage_treecompletes.D:\工作\项目\yy-subsystems\systems\infra\yy-agent-hub, from%TEMP%, and with a fresh home onC:\.Impact and verification
codex execis unaffected — it does not need the daemon. With the real~/.codexit completed and returnedPONG(exit 0, 35.7 s, modelgpt-6.1-sol).codex --yololaunched in the project directory stayed alive (20 s, no exit) and created a new session rollout (~10 MB). Before the workaround it exited in under a second withos error 5.codex app-server daemon versionthen reports{"status":"running",...,"appServerVersion":"0.159.2"}.This also independently confirms @shgef's correction in #48853: the release directory has to mirror the whole vendor tree, not just
bin\codex.exe.Reproduction
codex app-server daemon startreproduces without a tty, so it is easier to automate than the TUI path.Relationship to other reports
releases/state, and the same pre-staged-release workaround. The difference is the attributed failing call: that report puts it atFSCTL_SET_REPARSE_POINTinselect_release, whereas on 0.159.2 on this machine that call demonstrably succeeds and the failure is at the earlier rename. Possibly version-dependent, possibly one cause surfacing at different points.Environment
HTTP_PROXY/HTTPS_PROXY=http://127.0.0.1:7890