When PowerShell directly invokes a *.ps1
script, that script runs in-process, in the same runspace as the caller, and therefore the script by default sees the same current location (working directory) as the caller.
As an aside: Conversely, if a script changes the current location, the caller sees that too, after the script exits (see below for details).
If you do not see this behavior, the implication is that some behind-the-scenes code is changing the current location.
The only way I can realistically see this happen is in the following scenarios:
If (a) your $PROFILE
file contains a Set-Location
/ Push-Location
command that explicitly switches to your home folder or (b) (as in your case, as we now know) if there is - broken - code in an auto-loading module that does that on (automatic) import[1] and:
You call PowerShell via its CLI (powershell.exe
for Windows PowerShell, pwsh
for PowerShell Core) without the -NoProfile
switch; e.g. powershell -File someScript.ps1
On Unix-like platforms, your script is implemented and called as an extension-less, executable shell script with a shebang line (that doesn't include -NoProfile
- see below for details).
If you run your script via one of the following commands / features:
Via Start-Job
(in a child process) or Start-ThreadJob
(in a new runspace), at least currently (PowerShell Core 7.0.0-preview.4)
- Note: These two cmdlets don't behave exactly the same, which is problematic in and of itself, but the larger problem is that they don't simply inherit the caller's current location - see this GitHub issue.
Via the PowerShell SDK, in a new runspace.
Other than that, I can only see accidental event-based code producing your symptom, such as the following - pointless - redefinition of the interactive prompt-string defining function, prompt
, to make it quietly switch to the $HOME
directory after every command:
# !! Obviously, do NOT do this.
function prompt {
# Print the usual prompt string.
"PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "
# Quietly return to $HOME
Set-Location $HOME
}
The rest of the answer discusses a related scenario that may be of interest.
Executing a PowerShell script with its own directory as the working directory (current location):
In PowerShell v3+, automatic variable $PSScriptRoot
contains the full path of the directory in which the executing script is located.
If you need your script to execute with its own directory as the working directory (current location), use the following approach:
# Save the current location and switch to this script's directory.
# Note: This shouldn't fail; if it did, it would indicate a
# serious system-wide problem.
$prevPwd = $PWD; Set-Location -ErrorAction Stop -LiteralPath $PSScriptRoot
try {
# Your script's body here.
# ...
$PWD # output the current location
}
finally {
# Restore the previous location.
$prevPwd | Set-Location
}
Note:
The reason for explicitly restoring the previous location (directory) is that PowerShell runs scripts (.ps1
files) in-process, so if a script changes the current location with Set-Location
or Push-Location
, it takes effect session-globally; that is, the new location lingers even after the script exits.
On Unix-like platforms (Linux, macOS), with PowerShell Core, you now have the option of creating (extension-less) executable shell scripts with a shebang line; such scripts run in a child process, and there is therefore no need to restore the previous location (directory).
- Unfortunately, accessing information about the script's own invocation, including
$PSScriptRoot
, is broken in shebang-based scripts, as of PowerShell Core 7.0.0-preview.4, as discussed in this GitHub issue.
[1] No module should ever change the session-global state on import (aside from importing its commands), but it is technically possible, namely if you place commands such as Set-Location
in the top-level scope of a script module's *.psm1
file or in the scripts that run in the caller's scope via the ScriptsToProcess
module-manifest entry. Even binary cmdlets can execute code at import time, via the System.Management.Automation.IModuleAssemblyInitializer
interface - see this answer.