Windows standalone update from pwsh inherits PSModulePath into powershell.exe, causing Get-FileHash to fail
What happened?
While investigating a Windows standalone update failure, I found that the update action launches Windows PowerShell (powershell.exe) even when Codex itself is started from PowerShell 7 (pwsh). The child powershell.exe process can inherit PowerShell 7 entries in PSModulePath, which breaks module discovery in Windows PowerShell 5.1 and makes Get-FileHash unavailable.
The resulting installer failure looks like this:
Get-FileHash : The term 'Get-FileHash' is not recognized as the name of a cmdlet, function, script file, or operable program.
This does not appear to be caused by irm ... | iex corrupting the installer script text. I verified that the downloaded script text and piped script text are identical and parse successfully.
Environment
OS: Windows 10 22H2, 10.0.19045
Codex: codex-cli 0.138.0
Parent shell: pwsh 7.6.2
Child shell used by standalone update: powershell.exe 5.1.19041.6456
Codex executable: C:\Users\Administrator\AppData\Local\Programs\OpenAI\Codex\bin\codex.exe
Current main still has the Windows standalone update action hard-coded as:
UpdateAction::StandaloneWindows => (
"powershell",
&[
"-ExecutionPolicy",
"Bypass",
"-c",
"$env:CODEX_NON_INTERACTIVE=1; irm https://chatgpt.com/codex/install.ps1 | iex",
],
),
Observed on main commit:
08cb633c06a27d25872d0132fbd9c749556d7653
Minimal reproduction
This does not execute the Codex installer. It only simulates the relevant process-launch shape and checks whether Windows PowerShell can resolve and run Get-FileHash.
Run this from pwsh:
node - <<'NODE'
const { spawnSync } = require('child_process');
const psProbe = String.raw`
$ErrorActionPreference = 'Continue'
"PSVersion=$($PSVersionTable.PSVersion) Edition=$($PSVersionTable.PSEdition)"
"PSModulePath=$env:PSModulePath"
$cmd = Get-Command Get-FileHash -ErrorAction SilentlyContinue
"Get-FileHash found=$([bool]$cmd) type=$($cmd.CommandType) source=$($cmd.Source)"
try {
'Get-FileHash -LiteralPath $PSHOME\\powershell.exe -Algorithm SHA256 | Select-Object -First 1 Algorithm,Hash' | Invoke-Expression | Format-List
"InvokeExpressionResult=OK"
} catch {
"InvokeExpressionResult=FAIL"
"Error=$($_.Exception.Message)"
}
`;
const enc = Buffer.from(psProbe, 'utf16le').toString('base64');
const cleanWindowsPSModulePath = [
'C:\\Users\\Administrator\\Documents\\WindowsPowerShell\\Modules',
'C:\\Program Files\\WindowsPowerShell\\Modules',
'C:\\Windows\\system32\\WindowsPowerShell\\v1.0\\Modules',
].join(';');
const cases = [
['A. inherited env, like native child process', { ...process.env }],
['B. PSModulePath removed, let Windows PowerShell rebuild', (() => { const e = { ...process.env }; delete e.PSModulePath; delete e.PSMODULEPATH; return e; })()],
['C. clean WindowsPowerShell-only PSModulePath', { ...process.env, PSModulePath: cleanWindowsPSModulePath }],
];
for (const [name, env] of cases) {
const r = spawnSync('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', enc], {
encoding: 'utf8', timeout: 30000, env
});
console.log('\n==== ' + name + ' ====');
console.log('exit=' + r.status);
console.log(r.stdout.trim());
const stderr = r.stderr.replace(/#< CLIXML[\s\S]*/,'<CLIXML progress omitted>').trim();
if (stderr) console.log('stderr=' + stderr);
}
NODE
Actual output
With the inherited environment, Windows PowerShell 5.1 sees PowerShell 7 module paths first and cannot resolve Get-FileHash:
==== A. inherited env, like native child process ====
exit=0
PSVersion=5.1.19041.6456 Edition=Desktop
PSModulePath=C:\Users\Administrator\Documents\PowerShell\Modules;C:\Program Files\PowerShell\Modules;c:\program files\powershell\7\Modules;C:\Program Files\WindowsPowerShell\Modules;C:\Windows\system32\WindowsPowerShell\v1.0\Modules
Get-FileHash found=False type= source=
InvokeExpressionResult=FAIL
Error=The term 'Get-FileHash' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
If PSModulePath is removed before spawning powershell.exe, Windows PowerShell rebuilds a WindowsPowerShell-only module path and Get-FileHash works:
==== B. PSModulePath removed, let Windows PowerShell rebuild ====
exit=0
PSVersion=5.1.19041.6456 Edition=Desktop
PSModulePath=C:\Users\Administrator\Documents\WindowsPowerShell\Modules;C:\Program Files\WindowsPowerShell\Modules;C:\Windows\system32\WindowsPowerShell\v1.0\Modules
Get-FileHash found=True type=Function source=Microsoft.PowerShell.Utility
Algorithm : SHA256
Hash : 9785001B0DCF755EDDB8AF294A373C0B87B2498660F724E76C4D53F9C217C7A3
InvokeExpressionResult=OK
Codex-shaped safe reproduction
This mirrors the update action shape (powershell -ExecutionPolicy Bypass -c "... | iex") without running the installer:
==== codex-shaped command, inherited env ====
Get-FileHash : The term 'Get-FileHash' is not recognized as the name of a cmdlet, function, script file, or operable program.
==== codex-shaped command, PSModulePath removed ====
SHA256 9785001B0DCF755EDDB8AF294A373C0B87B2498660F724E76C4D53F9C217C7A3
Why this does not look like an irm | iex encoding problem
I also checked the real installer script without executing it. The direct Invoke-RestMethod result and the piped text had the same length, same SHA-256 over the text, same Get-FileHash index, and both parsed with zero PowerShell parser errors:
directLength=29883
pipeLength=29883
directSha256=ab7d7864801e62f92aa3abf878e3b0aaa8b9f7de03120bc81b029f67281c1842
pipeSha256=ab7d7864801e62f92aa3abf878e3b0aaa8b9f7de03120bc81b029f67281c1842
directParserErrors=0
pipeParserErrors=0
directGetFileHashIndex=2768
pipeGetFileHashIndex=2768
Expected behavior
The standalone Windows update should run reliably when Codex is started from pwsh, without making Windows PowerShell 5.1 inherit a PowerShell 7 module path that breaks module discovery.
Possible fixes
A few possible directions:
- Add
-NoProfileto the Windows standalone update invocation. - Remove or normalize
PSModulePathbefore spawningpowershell.exeforStandaloneWindows, so Windows PowerShell can rebuild its default WindowsPowerShell module path. - Prefer
pwshwhen Codex itself is running frompwshandpwshis available. - Add a fallback in
install.ps1that computes SHA-256 using .NET APIs ifGet-FileHashis unavailable.
The most targeted fix seems to be normalizing/removing PSModulePath for the powershell.exe child process used by the Windows standalone update action.
I am happy to help with a PR if maintainers agree with the preferred fix direction.
7 Comments
I am having this problem when trying to update codex from PowerShell 7, which is my main shell. The expectation is that
codex updateworks from any shellI can confirm this is still reproducible with a newer Codex release and the latest PowerShell 7 available in my environment.
Environment
10.0.26200.86557.6.30.141.00.142.5Reproduction
From PowerShell 7.6.3:
The updater launches Windows PowerShell rather than the current
pwshprocess:Running Windows PowerShell 5.1 independently with a clean environment resolves
Get-FileHashfromMicrosoft.PowerShell.Utility, which is consistent with the root-cause analysis in this issue: the failure is caused by the PowerShell 7PSModulePathinherited by thepowershell.exechild process, not byGet-FileHashbeing unavailable on the machine.Proposed fix
For the
StandaloneWindowsupdate action:powershell.exeas the compatibility baseline becausepwshis not guaranteed to be installed.-NoProfile.PSModulePathfrom the child process environment before spawningpowershell.exe, allowing Windows PowerShell to reconstruct its native module search path.PSModulePathand verifies that the child can resolve and executeGet-FileHash.A defensive .NET SHA-256 fallback in
install.ps1could provide an additional safety net, but normalizing the child environment appears to be the smallest fix at the source.This also appears to be the same failure reported in #30015. I would be happy to prepare the focused change and regression test if a maintainer would like to invite a PR under the repository's contribution policy.
I tested this solution (proposed by OP) and it worked great for me. If you're having issues updating codex cli I recommend trying it. 👍🏻
powershell -NoProfile -ExecutionPolicy Bypass -c '$env:CODEX_NON_INTERACTIVE=1; irm https://chatgpt.com/codex/install.ps1 | iex'I reproduced this on current
main(5c19155cbd93) on Windows, with PowerShell 7.6.3 spawning Windows PowerShell 5.1.26100.8457.With the inherited
PSModulePath,Get-Command Get-FileHashreturned no command. RemovingPSModulePathbefore spawning the same child restoredMicrosoft.PowerShell.Utility. Both probes used-NoProfile, so adding that flag alone is not sufficient.The current standalone Windows path in
codex-rs/cli/src/main.rsdirectly spawns the updater command without adjusting its environment. The smallest fix looks like adding.env_remove("PSModulePath")only forUpdateAction::StandaloneWindows, while keepingpowershell.exeand every other update path unchanged.I can keep the patch to that focused change and validate it with the PowerShell probe,
just test -p codex-cli,just fix -p codex-cli, andjust fmt.If this matches the intended direction, would a maintainer like me to open an invited PR for this issue?
Confirmed with a full isolated standalone update, not just a command-resolution probe.
Environment
10.0.26100.86557.6.30.142.50.144.15.1.26100.8655I copied the retained 0.142.5 standalone package into a temporary standalone layout, pointed
CODEX_HOMEandCODEX_INSTALL_DIRat isolated directories, and ran that binary's realcodex updatepath. The production installation was not used by the update.With the inherited environment, instrumentation in the child showed:
The real installer then failed at the archive verification step with:
I repeated the same isolated update after removing only
PSModulePathfrom the environment inherited bycodex.exe. The Windows PowerShell child reconstructed its native module path:The
Get-FileHashstep passed and Codex 0.144.1 was installed into the isolated target. This confirms that the narrow source fix is to removePSModulePathonly on theStandaloneWindowsupdater child process before spawningpowershell.exe.PowerShell's documented
pwsh -> powershell.exemodule-path translation does not apply here because nativecodex.exesits between the two shells:Adding
-NoProfilealone does not address the inherited process environment. I am preparing the focused.env_remove("PSModulePath")change and a unit-level command construction test. This is also the same user-visible failure reported in #30015.The focused implementation is ready for maintainer review:
010e24d3fc05b85ff039bbf82bcd41c5c81e1c5cdzshzx:codex/fix-windows-update-psmodulepathScope:
cli/src/main.rsinto a focused module;PSModulePathonly from theStandaloneWindowsPowerShell child;Validation:
I have not opened a pull request because
docs/contributing.mdsays external PRs require an explicit maintainer invitation and uninvited PRs are closed without review. If a maintainer confirms this direction and invites the PR, the branch is ready to open immediately.Confirmed independently on the current standalone release; this is still reproducible in Codex CLI 0.144.5.
Environment
7.6.35.1.26100.88750.144.4 -> 0.144.5User-visible reproduction
Running the ordinary command from PowerShell 7:
downloads the update, then fails during archive verification:
Opening Windows PowerShell 5.1 and running the same ordinary
codex updatecompleted successfully.Additional isolation
Both installed shells have a valid
Get-FileHash:Microsoft.PowerShell.Utility 7.0.Microsoft.PowerShell.Utility 3.1.Profiles and user/machine
PSModulePathconfiguration were checked; there are no custom module-path or autoload changes causing this.A read-only process-chain probe reproduced the boundary precisely:
That matches the production chain:
The direct PowerShell 7-to-Windows PowerShell launch performs PowerShell's documented
PSModulePathcompatibility translation. Inserting a native process preserves the PowerShell 7 process environment and bypasses that handoff.This confirms the report and the focused fix already prepared in this thread: remove
PSModulePathfrom theStandaloneWindowsupdater child before spawningpowershell.exe. The expected behavior is that the literalcodex updatecommand works from PowerShell 7 without profiles, wrappers, alternate commands, or switching shells.