Skip to content

Windows PowerShell 5.1 grandchild inherits pwsh 7's PSModulePath and loads 7.0.0.0 modules: Get-FileHash throws CommandNotFoundException #27774

Description

Prerequisites

Summary

pwsh sanitizes PSModulePath for the Windows PowerShell processes it launches directly, but the sanitization does not survive an intermediate process. When powershell.exe (5.1) is launched as a grandchild — for example pwsh → python → powershell.exe — it inherits pwsh's raw PSModulePath. The PowerShell 7 module directory precedes the Windows PowerShell system module directory, so 5.1 resolves shared module names (Microsoft.PowerShell.Utility, Microsoft.PowerShell.Management, …) to the 7.0.0.0 builds and module auto-loading fails.

Net effect: Get-FileHash — and every other cmdlet from those modules — raises CommandNotFoundException in a fully normal Windows PowerShell 5.1 session.

The direct-launch case works, which makes this hard to diagnose: the defect appears only when an intermediate process sits between pwsh and powershell.exe.

Steps to reproduce

test51.ps1 (ASCII, no BOM issues):

$PSVersionTable.PSVersion.ToString()
"PSModulePath entries : " + (($env:PSModulePath -split ';') | Where-Object { $_ }).Count
try {
    $h = Get-FileHash -LiteralPath "$env:SystemRoot\notepad.exe" -Algorithm SHA256
    "GET-FILEHASH OK " + $h.Hash.Substring(0, 16)
} catch {
    "GET-FILEHASH ECHEC : " + $_.Exception.GetType().Name
}

From a pwsh 7.6.4 session:

$ps51 = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"

# 1. direct child
& $ps51 -NoProfile -NonInteractive -File .\test51.ps1

# 2. grandchild through Python (any intermediate process reproduces it)
python -c "import subprocess; print(subprocess.run([r'$ps51','-NoProfile','-NonInteractive','-File',r'.\test51.ps1'],capture_output=True,text=True).stdout)"

Expected behavior

5.1.26100.8972
PSModulePath entries : 4
GET-FILEHASH OK 3F543719A819A976

Actual behavior

Direct child — correct:

5.1.26100.8972
PSModulePath entries : 4
GET-FILEHASH OK 3F543719A819A976

Grandchild through Python — broken:

5.1.26100.8972
PSModulePath entries : 6
GET-FILEHASH ECHEC : CommandNotFoundException

Mechanism

PSModulePath as seen by pwsh 7.6.4 (6 entries):

C:\Users\<user>\Documents\PowerShell\Modules
C:\Program Files\PowerShell\Modules
c:\program files\windowsapps\microsoft.powershell_7.6.4.0_x64__8wekyb3d8bbwe\Modules   <-- pwsh 7 modules
C:\Program Files\WindowsPowerShell\Modules
C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules                                      <-- 5.1 system modules
C:\Program Files\Intel\Wired Networking\

PSModulePath as seen by a direct powershell.exe 5.1 child (4 entries) — pwsh correctly rewrites it:

C:\Users\<user>\Documents\WindowsPowerShell\Modules     (substituted for the PowerShell one)
C:\Program Files\WindowsPowerShell\Modules
C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules
C:\Program Files\Intel\Wired Networking\

Entries removed by pwsh for its direct child:

C:\Users\<user>\Documents\PowerShell\Modules
C:\Program Files\PowerShell\Modules
c:\program files\windowsapps\microsoft.powershell_7.6.4.0_x64__8wekyb3d8bbwe\Modules

So the sanitization exists and is correct — it simply does not apply when the 5.1 process is not a direct child.

The pwsh 7 module directory holds 8 modules whose names collide with Windows PowerShell 5.1 modules:

Microsoft.PowerShell.Archive      Microsoft.PowerShell.Security
Microsoft.PowerShell.Diagnostics  Microsoft.PowerShell.ThreadJob
Microsoft.PowerShell.Host         Microsoft.PowerShell.Utility
Microsoft.PowerShell.Management   Microsoft.PowerShell.PSResourceGet

What 5.1 resolves, with the inherited path:

PS> Get-Module -ListAvailable Microsoft.PowerShell.Utility

7.0.0.0   C:\program files\windowsapps\...\Modules\Microsoft.PowerShell.Utility\Microsoft.PowerShell.Utility.psd1
3.1.0.0   C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\Microsoft.PowerShell.Utility\Microsoft.PowerShell.Utility.psd1

With PSModulePath removed from the child environment, only 3.1.0.0 is listed and everything works.

Why this matters

The symptom is opaque. A test harness written in Python that shells out to powershell.exe fails uniformly with a non-zero exit code that looks like a defect in the harness or in the script under test. In my case an entire 38-case suite failed identically before the cause was found, and the direct-launch check — the natural thing to try first — showed nothing wrong.

Any tool that runs user code under pwsh reproduces this: build systems, CI agents, editors' integrated terminals, agentic coding tools.

Workaround

Strip PSModulePath from the subprocess environment; the 5.1 child then rebuilds its own system default:

env = {k: v for k, v in os.environ.items() if k.upper() != "PSMODULEPATH"}
subprocess.run([ps51, "-NoProfile", "-File", script], env=env)

Possible fixes

  1. Have powershell.exe 5.1 detect and drop PowerShell-Core-only module directories from an inherited PSModulePath — arguably the robust fix, but it lives in the Windows PowerShell codebase.
  2. Have pwsh avoid exporting a Core-only PSModulePath into the environment it passes to all children, not just Windows PowerShell ones — e.g. keep the Core-specific entries in the session rather than in the process environment block.
  3. At minimum, document it: the "Differences between Windows PowerShell 5.1 and PowerShell 7" page covers PSModulePath differences, but not that they leak to grandchildren and shadow 5.1's own modules.

Environment data

Name                           Value
----                           -----
PSVersion                      7.6.4
PSEdition                      Core
GitCommitId                    7.6.4
OS                             Microsoft Windows 10.0.26200
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
PSRemotingProtocolVersion      2.4
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

Additional:

pwsh install     : MSIX / Microsoft Store
                   C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.4.0_x64__8wekyb3d8bbwe\pwsh.exe
Windows PowerShell: 5.1.26100.8972
OS               : Microsoft Windows 11 Enterprise, 10.0.26200
Intermediate     : Python 3.14.3 (any intermediate process reproduces it)

Note: the pwsh install here is the MSIX/Store build, which is why the offending directory sits under WindowsApps. The same shadowing is expected with an MSI install, where the directory would be C:\Program Files\PowerShell\7\Modules.

Activity

  1. doctordns commented on Aug 6, 2026

    @doctordns
    Collaborator

    This would appear to be an issue with Windows Powershell. This repository, however, is for Powershell (core). Thus, this issue should be closed and an issue raised with the Windows Feedback tool. Sorry if this is not the answer you wanted.

  2. Sylvain13bdr commented on Aug 6, 2026

    @Sylvain13bdr
    Author

    Thanks for looking at this, and the repo-scope point is fair.

    One measured detail from the report may change the framing, though: PowerShell Core already implements the mitigation. When pwsh launches powershell.exe directly, it rewrites PSModulePath — 6 entries become 4 — and it even substitutes Documents\PowerShell\Modules with Documents\WindowsPowerShell\Modules:

    PSModulePath in pwsh 7.6.4 (6 entries)
      C:\Users\<user>\Documents\PowerShell\Modules
      C:\Program Files\PowerShell\Modules
      c:\program files\windowsapps\microsoft.powershell_7.6.4.0_x64__8wekyb3d8bbwe\Modules
      C:\Program Files\WindowsPowerShell\Modules
      C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules
      C:\Program Files\Intel\Wired Networking\
    
    PSModulePath in a DIRECT powershell.exe 5.1 child (4 entries)
      C:\Users\<user>\Documents\WindowsPowerShell\Modules      <-- substituted
      C:\Program Files\WindowsPowerShell\Modules
      C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules
      C:\Program Files\Intel\Wired Networking\
    

    That logic lives in Core, and it means Core has already concluded that its PSModulePath is unsafe for Windows PowerShell, and owns the mitigation.

    What is missing is therefore not a Windows PowerShell fix. It is that Core applies that sanitization only at the moment it spawns the process, while exporting the unsanitized value into the environment block that every descendant inherits. A grandchild — pwsh → any intermediate process → powershell.exe — receives the raw value and resolves Microsoft.PowerShell.Utility to 7.0.0.0.

    So the question I would put to the team is a Core design one: should Core-only module directories live in the process environment block at all, rather than in session state?

    If the team still considers this out of scope, I will close it and raise it through Feedback Hub — no argument. I just wanted the existing sanitization on record first, since it suggests the boundary is already drawn on the Core side.

  3. surfingoldelephant commented on Aug 6, 2026

    @surfingoldelephant

    Duplicate of:

    This current issue does describe the issue in more detail though.

  4. added
    Issue-BugIssue has been identified as a bug in the product
    WG-Enginecore PowerShell engine, interpreter, and runtime
    WG-NeedsReviewNeeds a review by the labeled Working Group
    on Aug 6, 2026
  5. kilasuit commented on Aug 6, 2026

    @kilasuit
    Collaborator

    My question always when I see this sort of thing (irrespective of the PowerShell vs pwsh) is where is the benefit in the multiple layered approach going to grandchild/great-granchild/great-great-granchild etc

    Using Start-Process as opposed to direct calling of the python executable may help here with the -UseNewEnvironment parameter (not tested this as I don't have a python install to hand)

    I don't think we could do anything in this codebase to aid with pwsh -> other executable/s -> powershell other than perhaps provide guidance which would be docs updates.

    I am marking #18108 as dupe of this as the title in here is clearer & copied the labels that had across.

  6. Sylvain13bdr commented on Aug 7, 2026

    @Sylvain13bdr
    Author

    Thanks Ryan Yates (@kilasuit), and thanks for consolidating #18108 here.

    I measured your suggestion rather than assume anything about it. It works. Full matrix below, run today on the machine from the report — pwsh 7.6.4 (MSIX), Windows PowerShell 5.1.26100.8972, Python 3.14.3, Windows 11 10.0.26200. test51.ps1 is unchanged from the original report.

    # chain PSModulePath seen by 5.1 Get-FileHash session env
    A pwsh -> powershell.exe 4 OK —
    B pwsh -> python -> powershell.exe 6 CommandNotFoundException preserved
    C B, with PSModulePath stripped in Python 4 OK preserved
    D pwsh -> Start-Process python -Environment @{PSModulePath=$null} -> powershell.exe 4 OK preserved
    E pwsh -> Start-Process python -UseNewEnvironment -> powershell.exe 4 OK wiped
    F pwsh -> cmd.exe -> powershell.exe 6 CommandNotFoundException —
    G pwsh -> cmd.exe -> cmd.exe -> powershell.exe 6 CommandNotFoundException —
    H pwsh -> cmd /c "set PSModulePath=&& …" -> powershell.exe 4 OK —

    On -UseNewEnvironment (E)

    It does fix the symptom here, so the suggestion is sound. One scoping note: PSModulePath at Machine scope on this machine holds only the three Windows PowerShell entries and User scope is empty, so failure mode 2 from jazzdelightsme's write-up in #18108 — a registry value already polluted with version-specific paths — does not apply here and would need separate testing.

    Two measured costs:

    • It resets the whole environment. I set a marker variable in the pwsh session before launching. Case D still sees PROBE_SESSION_VAR=PRESENT; case E sees <ABSENT>. For the Python test harness this report came from, that also removes VIRTUAL_ENV and the venv PATH — the workaround breaks the thing it is fixing.
    • The value handed to the intermediate is %ProgramFiles%\WindowsPowerShell\Modules, unexpanded. 5.1 normalizes it, but a non-PowerShell consumer reading PSModulePath gets the literal.

    A better form of the same idea (D)

    Start-Process gained -Environment in PowerShell 7.4, where a $null value removes a single variable and inherits the rest:

    Start-Process python -Environment @{ PSModulePath = $null } -NoNewWindow -Wait

    Measured: PSModulePath absent in the intermediate, 5.1 rebuilds its own 4 entries, Get-FileHash OK, and the rest of the session environment survives. This is the PowerShell-native equivalent of the Python workaround in the report, and I think it is the better line to put in any guidance that comes out of this.

    On "where is the benefit in the multiple layered approach"

    This is the part I would gently push back on, and F/G are the measured answer: no Python, no layering, no design choice — a single cmd /c reproduces it, and a second nested cmd changes nothing.

    That is exactly the chain reported in #18108, and it is what a VS Code integrated terminal running pwsh produces the moment a Makefile, an npm script, or a CI step shells out to powershell.exe. Nobody chooses to have a grandchild; it is the default shape of ordinary tooling. The Python intermediate in my report is incidental — it is simply what my harness happened to be written in.

    What is actually missing

    H confirms the 2022 set PSModulePath= workaround still works. So there are three known fixes — Python-side, Start-Process-side, cmd-side — and every one of them is caller-side and requires the caller to already know about the defect. A workaround was never the missing piece; the report shipped with one.

    The gap is that an intermediate process has no supported way to hand a correct environment to a Windows PowerShell descendant, even though Core already computes exactly that value at spawn time — case A is 6 entries in, 4 out, with Documents\PowerShell\Modules substituted for Documents\WindowsPowerShell\Modules.

    AssortedBits asked for precisely this in #18108 and it was never answered: either document it as known and unsupported, or expose the fixup. The second is a small change — make the sanitized value Core already computes reachable, so an intermediate can apply it deliberately. That sidesteps the "we cannot control what grandchildren inherit" objection entirely, because the grandchild's parent would then have something supported to call.

    If the WG concludes docs-only, that is a fine outcome by me — it would just help to name the target so this is closeable. Concretely: a short section in about_PSModulePath stating that the Windows PowerShell fixup applies only to direct children and does not survive an intermediate process, with the three workarounds above.

    I am happy to open the docs PR against MicrosoftDocs/PowerShell-Docs with that text if the WG prefers that route.

  7. rhubarb-geek-nz commented on Aug 14, 2026

    @rhubarb-geek-nz

    If you remember IRIX, ("It's a UNIX system! I know this!"), it had separate library path variables for different ELF formats, LD_LIBRARY_PATH, LD_LIBRARYN32_PATH and LD_LIBRARY64_PATH.

    In retrospect PowerShell 7 should have kept PSModulePath purely for PowerShell Desktop compatible modules, and put PowerShell 7 ones on a path called PSModulePath7 or PSModulePathCore.

    Also, it should not be messing with the PATH variable and adding itself to the PATH.

    If that had been done the nesting could have been have been continual until memory limits of the machine were hit.

  8. daxian-dbw commented on Aug 24, 2026

    @daxian-dbw
    Member

    The Engine WG reviewed this issue and we believe this is a by-design behavior for pwsh. As for powershell.exe, we won't be able to change its module path discover behavior. We agree that the doc can be enhanced to call out this behavior in about_PSModulePath article. Sylvain Labeye (@Sylvain13bdr), please open a doc issue or PR at your convivence. Thanks!

  9. added
    WG-ReviewedA Working Group has reviewed this and made a recommendation
    and removed
    WG-NeedsReviewNeeds a review by the labeled Working Group
    on Aug 24, 2026
  10. Sylvain13bdr commented on Aug 25, 2026

    @Sylvain13bdr
    Author

    Doc PR opened as invited: MicrosoftDocs/PowerShell-Docs#13248

    It extends "Starting Windows PowerShell from PowerShell 7" in
    about_PSModulePath (7.4 through 7.7) with the direct-children limitation
    and the three measured workarounds (Start-Process -Environment on 7.4+,
    cmd.exe set PSModulePath=, Python subprocess with the variable removed),
    and adds an inheriting-side note to the 5.1 article. Thanks for the review,
    Dongbo Wang (@daxian-dbw).

  11. sdwheeler commented on Aug 25, 2026

    @sdwheeler
    Contributor
  12. kilasuit commented on Aug 28, 2026

    @kilasuit
    Collaborator
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Issue-BugIssue has been identified as a bug in the productResolution-By DesignThe reported behavior is by design.WG-Enginecore PowerShell engine, interpreter, and runtimeWG-ReviewedA Working Group has reviewed this and made a recommendation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions