Windows PATH and PowerShell: How Command Resolution Actually Works
- Understand how Windows resolves commands in PowerShell using the PATH environment variable
- Add directories to PATH permanently at the system and user scope
- Diagnose common PATH problems with PowerShell
When you type python in PowerShell and press Enter, Windows doesn't search your entire hard drive. It looks through a list of directories called PATH, in order, and runs the first executable it finds with that name. Understanding how PATH is structured — and how PowerShell reads and modifies it — eliminates a category of "command not found" errors that stump most Windows users.
What Is PATH?
PATH is a Windows environment variable that holds a semicolon-separated list of directories. When you run a command without a full path, Windows searches each directory in the list left-to-right until it finds a matching .exe, .cmd, .bat, or .ps1 file.
C:\Windows\System32;C:\Windows;C:\Program Files\Git\bin;C:\Users\You\AppData\Local\Microsoft\WindowsApps
PowerShell inherits PATH from two sources: the system-wide PATH (set in HKLM registry, applies to all users) and the user-level PATH (set in HKCU registry, applies to the current user). Windows merges them — user PATH is appended to system PATH — before exposing the combined result as $env:PATH inside your session.
That means PATH is not a database of installed programs. It is only a search list. If C:\Tools contains ripgrep.exe but C:\Tools is not in PATH, typing ripgrep fails even though the file exists. If you run C:\Tools\ripgrep.exe with the full path, PATH is bypassed entirely. The common mistake is reinstalling a tool when the real problem is that the directory containing the executable was never added to either user PATH or machine PATH.
The order matters as much as the contents. If two directories contain python.exe, the first matching directory in the merged PATH wins. A user-level Python installed under C:\Users\You\AppData\Local\Programs\Python can beat, or lose to, an older system-level Python depending on the final merged order. PowerShell's Get-Command is the fastest way to prove which executable actually wins because it resolves the command the way the shell will resolve it [5].
How Windows Resolves a PowerShell Command
The diagram below traces what happens when you type a command in PowerShell.
sequenceDiagram
title Windows PATH Resolution in PowerShell
actor User
participant PS as PowerShell
participant ENV as Environment Block
participant REG as Registry (HKLM + HKCU)
participant FS as File System
User->>PS: Type command (e.g. python)
PS->>ENV: Read $env:PATH
ENV->>REG: Load HKLM\SYSTEM\...\PATH (system scope)
REG-->>ENV: System PATH string
ENV->>REG: Load HKCU\Environment\PATH (user scope)
REG-->>ENV: User PATH string
ENV-->>PS: Combined PATH (system + user, semicolon-joined)
loop Each directory in PATH order
PS->>FS: Look for python.exe / python.cmd / python.bat
FS-->>PS: Found? → execute and stop / Not found? → next dir
end
PS-->>User: Run executable or "not recognized" error
Figure 1 — Windows PATH resolution sequence in PowerShell. System PATH loads first from the HKLM registry hive; user PATH loads from HKCU and is appended. PowerShell searches each directory in the merged list in order. The first match wins.
Key behaviour: the session's $env:PATH is a snapshot taken when the PowerShell process started. Changes you make in another session (including via System Properties) are not visible until you open a new PowerShell window — unless you reload the variable explicitly.
PowerShell Checks Shell Commands Before PATH
PATH is only one layer of PowerShell command resolution. Before PowerShell launches an external executable from PATH, it can resolve built-in command types such as aliases, functions, cmdlets, scripts, and applications. That is why Get-Command is more useful than manually reading $env:PATH: it reports the actual command type and source PowerShell will use [5].
For example, where in PowerShell may not behave like where.exe in cmd.exe because PowerShell has its own aliases and command discovery rules. When you need the external Windows executable, call it explicitly as where.exe. When you need to know whether a name is an alias, function, cmdlet, or external application, run:
Get-Command where -All
Get-Command python -All
The -All flag is useful for shadowing bugs. It shows every command PowerShell can find with that name, not just the winner. If an old node.exe appears before the version you just installed, the fix is not "repair Node"; it is to move, remove, or correct the earlier PATH entry.
How to View Your Current PATH
# View the combined PATH (system + user merged)
$env:PATH -split ';'
# View system PATH only
[Environment]::GetEnvironmentVariable('PATH', 'Machine')
# View user PATH only
[Environment]::GetEnvironmentVariable('PATH', 'User')
The -split ';' converts the semicolon-delimited string into an array, making it easier to read and search.
Adding a Directory to PATH
Temporary (current session only)
$env:PATH += ";C:\MyTools"
This modifies the in-memory environment block for the current PowerShell process. It is lost when the session closes.
Permanent (user scope, persists across sessions)
$current = [Environment]::GetEnvironmentVariable('PATH', 'User')
[Environment]::SetEnvironmentVariable('PATH', "$current;C:\MyTools", 'User')
Changes at user scope write to HKCU\Environment. Windows broadcasts a WM_SETTINGCHANGE message so open applications can reload; PowerShell sessions that are already running will NOT automatically pick up the change. Open a new window to see it.
Permanent (system scope, requires admin)
# Run PowerShell as Administrator
$current = [Environment]::GetEnvironmentVariable('PATH', 'Machine')
[Environment]::SetEnvironmentVariable('PATH', "$current;C:\SharedTools", 'Machine')
System scope writes to HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment. This affects all users on the machine [6].
Diagnosing PATH Problems
Problem: `python` is not recognised
# Find where python.exe is hiding
Get-Command python -ErrorAction SilentlyContinue
# Check if it's in PATH at all
$env:PATH -split ';' | Where-Object { Test-Path "$_\python.exe" }
Problem: wrong version of a tool is running
# See which python.exe wins the PATH race
Get-Command python | Select-Object -ExpandProperty Source
Problem: a PATH change isn't visible in the current session
# Reload user PATH into current session without restarting
$env:PATH = [Environment]::GetEnvironmentVariable('PATH','Machine') + ';' +
[Environment]::GetEnvironmentVariable('PATH','User')
Problem: PATH is too long (Windows has a 2048-character limit for user PATH in some tools)
$env:PATH.Length
# If > 2000, audit and remove stale directories
$env:PATH -split ';' | Where-Object { -not (Test-Path $_) }
Problem: PATH contains duplicates
$env:PATH -split ';' |
Where-Object { $_ } |
Group-Object |
Where-Object Count -gt 1 |
Select-Object Count, Name
Duplicates usually do not break command resolution, but they make PATH harder to reason about and waste space in the environment block. Remove duplicate entries from the user or machine scope that actually owns them instead of editing only $env:PATH, because $env:PATH changes disappear when the current process exits [1].
Stale entries are more important than duplicates. A deleted tool directory near the front of PATH forces every command lookup to test a location that can never match, and a stale entry can hide the real issue when a later installer adds the correct directory. A quick health check is to split PATH, drop empty strings, and test each directory:
$env:PATH -split ';' |
Where-Object { $_ } |
Where-Object { -not (Test-Path $_) }
If this returns old SDK folders, deleted package-manager directories, or typoed paths, remove them from the user or machine scope where they are stored. That keeps the permanent PATH shorter, easier to audit, and less likely to hit older tools that still impose practical length limits.
Problem: a directory contains spaces
[Environment]::SetEnvironmentVariable(
'PATH',
"$([Environment]::GetEnvironmentVariable('PATH','User'));C:\Program Files\My Tool\bin",
'User'
)
Do not wrap PATH entries in quotes just because the directory has spaces. PATH is split on semicolons, not spaces. Quoting the directory can become part of the stored value and cause tool lookup bugs in programs that do not normalize entries the same way PowerShell does.
Virtual Environments Change PATH Temporarily
Python virtual environments, Conda environments, Node version managers, and similar tools usually work by prepending a tool-specific directory to the current process PATH. That is intentional. Activating a Python virtual environment does not rewrite your permanent user PATH; it changes the current shell session so python resolves to the environment-local interpreter first.
You can see the effect directly:
Get-Command python | Select-Object -ExpandProperty Source
.\.venv\Scripts\Activate.ps1
Get-Command python | Select-Object -ExpandProperty Source
Before activation, python may resolve to the Windows Store shim, a system install, or a user install. After activation, it should resolve under .\.venv\Scripts\python.exe. When you close the terminal or deactivate the environment, that temporary PATH prefix goes away because it was process-scoped, not registry-scoped [1].
This is also why "fixing" virtual environment PATH changes by writing .venv\Scripts into your permanent user PATH is usually wrong. It makes one project's interpreter leak into unrelated shells. Treat project environments as session-local state: activate them when you work in the project, then let the activation script manage the PATH prefix.
Environment Variable Scopes at a Glance
| Scope | Registry hive | Who can modify | When effective |
|---|---|---|---|
| Process | In-memory only | Current process | Immediately, current session |
| User | HKCU\Environment | Current user | New sessions |
| Machine (system) | HKLM\SYSTEM\...\Environment | Administrators | New sessions (all users) |
[Environment]::SetEnvironmentVariable .NET method works identically in both [4]. If you use winget or scoop to install tools, they typically update the user PATH automatically — but you still need a new PowerShell window to see the change.Learn More
- Claude Tool Use From Zero — build automation agents that run PowerShell commands via tool use.
- Secure Coding With Claude — best practices for safe scripting, including environment variable handling.
References
- PowerShell Docs — About Environment Variables· retrieved 2026-07-08
- Windows Docs — Environment Variables· retrieved 2026-07-08
- PowerShell — String Substitutions Deep Dive· retrieved 2026-07-08
- PowerShell — About Environment Providers· retrieved 2026-07-13
- PowerShell — Get-Command· retrieved 2026-07-09
- PowerShell — About Registry Provider· retrieved 2026-07-09