Windows standalone update from pwsh inherits PSModulePath into powershell.exe, causing Get-FileHash to fail

Open 💬 7 comments Opened Jun 9, 2026 by BlueOcean223

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:

  1. Add -NoProfile to the Windows standalone update invocation.
  2. Remove or normalize PSModulePath before spawning powershell.exe for StandaloneWindows, so Windows PowerShell can rebuild its default WindowsPowerShell module path.
  3. Prefer pwsh when Codex itself is running from pwsh and pwsh is available.
  4. Add a fallback in install.ps1 that computes SHA-256 using .NET APIs if Get-FileHash is 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.

View original on GitHub ↗

7 Comments

thomak-dev · 1 month ago

I am having this problem when trying to update codex from PowerShell 7, which is my main shell. The expectation is that codex update works from any shell

rteoo · 18 days ago

I can confirm this is still reproducible with a newer Codex release and the latest PowerShell 7 available in my environment.

Environment

  • Windows: 10.0.26200.8655
  • Parent shell: PowerShell 7.6.3
  • Codex CLI: 0.141.0
  • Target update: 0.142.5

Reproduction

From PowerShell 7.6.3:

codex update

The updater launches Windows PowerShell rather than the current pwsh process:

Updating Codex via `powershell -ExecutionPolicy Bypass -c '$env:CODEX_NON_INTERACTIVE=1; irm https://chatgpt.com/codex/install.ps1 | iex'`...
==> Updating Codex CLI from 0.141.0 to 0.142.5
==> Detected platform: Windows (x64)
==> Resolved version: 0.142.5
==> Downloading Codex CLI
iex : The term 'Get-FileHash' is not recognized as the name of a cmdlet, function, script file, or operable program.

Running Windows PowerShell 5.1 independently with a clean environment resolves Get-FileHash from Microsoft.PowerShell.Utility, which is consistent with the root-cause analysis in this issue: the failure is caused by the PowerShell 7 PSModulePath inherited by the powershell.exe child process, not by Get-FileHash being unavailable on the machine.

Proposed fix

For the StandaloneWindows update action:

  1. Keep powershell.exe as the compatibility baseline because pwsh is not guaranteed to be installed.
  2. Add -NoProfile.
  3. Remove PSModulePath from the child process environment before spawning powershell.exe, allowing Windows PowerShell to reconstruct its native module search path.
  4. Add a Windows regression test that starts the update command with a PowerShell 7-style PSModulePath and verifies that the child can resolve and execute Get-FileHash.

A defensive .NET SHA-256 fallback in install.ps1 could 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.

duckyb · 14 days ago

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'

balloon72 · 9 days ago

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-FileHash returned no command. Removing PSModulePath before spawning the same child restored Microsoft.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.rs directly spawns the updater command without adjusting its environment. The smallest fix looks like adding .env_remove("PSModulePath") only for UpdateAction::StandaloneWindows, while keeping powershell.exe and 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, and just fmt.

If this matches the intended direction, would a maintainer like me to open an invited PR for this issue?

dzshzx · 9 days ago

Confirmed with a full isolated standalone update, not just a command-resolution probe.

Environment

  • Windows 11 x64: 10.0.26100.8655
  • Parent shell: PowerShell 7.6.3
  • Source Codex: 0.142.5
  • Target Codex: 0.144.1
  • Child updater shell: Windows PowerShell 5.1.26100.8655

I copied the retained 0.142.5 standalone package into a temporary standalone layout, pointed CODEX_HOME and CODEX_INSTALL_DIR at isolated directories, and ran that binary's real codex update path. The production installation was not used by the update.

With the inherited environment, instrumentation in the child showed:

DIAG_PARENT=codex.exe
DIAG_PSHOME=C:\Windows\System32\WindowsPowerShell\v1.0
DIAG_PSMODULEPATH=...;C:\Program Files\PowerShell\7\Modules;...;C:\Windows\System32\WindowsPowerShell\v1.0\Modules

Name    : Microsoft.PowerShell.Utility
Version : 7.0.0.0
Path    : C:\Program Files\PowerShell\7\Modules\Microsoft.PowerShell.Utility\...

The real installer then failed at the archive verification step with:

Get-FileHash : The term 'Get-FileHash' is not recognized

I repeated the same isolated update after removing only PSModulePath from the environment inherited by codex.exe. The Windows PowerShell child reconstructed its native module path:

DIAG_PSMODULEPATH=...\Documents\WindowsPowerShell\Modules;C:\Program Files\WindowsPowerShell\Modules;C:\Windows\System32\WindowsPowerShell\v1.0\Modules

Name    : Microsoft.PowerShell.Utility
Version : 3.1.0.0
Path    : C:\Windows\System32\WindowsPowerShell\v1.0\Modules\Microsoft.PowerShell.Utility\...

The Get-FileHash step passed and Codex 0.144.1 was installed into the isolated target. This confirms that the narrow source fix is to remove PSModulePath only on the StandaloneWindows updater child process before spawning powershell.exe.

PowerShell's documented pwsh -> powershell.exe module-path translation does not apply here because native codex.exe sits between the two shells:

pwsh.exe -> codex.exe -> powershell.exe

Adding -NoProfile alone 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.

dzshzx · 9 days ago

The focused implementation is ready for maintainer review:

Scope:

  • move the existing update-process runner out of the 4,000-line cli/src/main.rs into a focused module;
  • remove PSModulePath only from the StandaloneWindows PowerShell child;
  • keep all other update paths unchanged;
  • add a command-construction regression test that asserts the child environment removal.

Validation:

just test -p codex-cli
295 tests run: 295 passed, 0 skipped

just fix -p codex-cli
passed

just fmt
passed

git diff --check
passed

I have not opened a pull request because docs/contributing.md says 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.

galar71 · 4 days ago

Confirmed independently on the current standalone release; this is still reproducible in Codex CLI 0.144.5.

Environment

  • Windows 11 x64
  • Parent shell: PowerShell 7.6.3
  • Windows PowerShell child: 5.1.26100.8875
  • Standalone Codex update: 0.144.4 -> 0.144.5
  • Authentication: ChatGPT account (no API-key installation path involved)

User-visible reproduction

Running the ordinary command from PowerShell 7:

codex update

downloads the update, then fails during archive verification:

iex : The term 'Get-FileHash' is not recognized as the name of a cmdlet,
function, script file, or operable program.
...
Error: `powershell -ExecutionPolicy Bypass -c '$env:CODEX_NON_INTERACTIVE=1; irm https://chatgpt.com/codex/install.ps1 | iex'` failed with status exit code: 1

Opening Windows PowerShell 5.1 and running the same ordinary codex update completed successfully.

Additional isolation

Both installed shells have a valid Get-FileHash:

  • PowerShell 7 resolves it from Microsoft.PowerShell.Utility 7.0.
  • Windows PowerShell 5.1 resolves it from Microsoft.PowerShell.Utility 3.1.

Profiles and user/machine PSModulePath configuration were checked; there are no custom module-path or autoload changes causing this.

A read-only process-chain probe reproduced the boundary precisely:

pwsh.exe -> powershell.exe
Get-FileHash: Microsoft.PowerShell.Utility

pwsh.exe -> native intermediary -> powershell.exe
Get-FileHash: not found

That matches the production chain:

pwsh.exe -> codex.exe -> powershell.exe

The direct PowerShell 7-to-Windows PowerShell launch performs PowerShell's documented PSModulePath compatibility 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 PSModulePath from the StandaloneWindows updater child before spawning powershell.exe. The expected behavior is that the literal codex update command works from PowerShell 7 without profiles, wrappers, alternate commands, or switching shells.