Skip to content

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

Description

@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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    CLIIssues related to the Codex CLIbugSomething isn't workingwindows-osIssues related to Codex on Windows systems

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions