Search code examples
windowspowershellwindows-terminal

How to start another PowerShell session in a separate window while keeping the environment?


While working in PowerShell I tend to quickly switch to admin mode by typing

Start-Process wt -Verb runas

When I do so, a new window appears (sadly, no sudo in Windows). In that new session however, the environment is totally fresh. Is it possible to keep variables, aliases, working dir and all other stuff of similar matter while jumping to a new window? If not, then well, it's a valid answer.


To give some example, I am looking for this behavior:

First window

C:\test> $x = 123
C:\test> Start-Process wt

New window

C:\test> $x
123

Solution

  • By design, elevated sessions (-Verb RunAs) do not inherit the caller's environment variables and even explicit attempts to pass an environment are categorically prevented by the underlying .NET API.[1]

    Also, whether or not you use -Verb RunAs, the state of a PowerShell session (aliases, functions, shell-only variables, ...) is never inherited when you launch another PowerShell process, such as with Start-Process.


    A workaround is to explicitly and selectively recreate the state of interest via commands executed in the elevated session, based on values from the caller's state, but that is quite cumbersome and has limitations, as the following example shows:

    # Define a few things to copy to the elevated session.
    $x1 = 123
    $x2 = '3" of snow' # !! See the caveat re regular variables below.
    $env:foo = 1
    $env:foo2 = 2
    Set-Alias bar Get-Date
    function baz { "hello, world" }
    
    # Note: The following only copies the definitions above.
    #       You could try to copy ALL such definitions, by omitting a target name / pattern:
    #         Get-ChildItem env:
    #         Get-ChildItem function:
    #         Get-ChildItem alias:
    #       CAVEAT: This will NOT generally work with *regular variables*.
    Start-Process -Verb RunAs powershell @"
    -NoExit -Command Set-Location -LiteralPath \"$((Get-Location -PSProvider FileSystem).ProviderPath)\"
    $(Get-Variable x? | ForEach-Object { "`${$($_.Name)} = $(if ($_.Value -is [string]) { "'{0}'" -f ($_.Value -replace "'", "''" -replace '"', '\"')  } else { $_.Value }); " })
    $(Get-ChildItem env:foo* | ForEach-Object { "Set-Item \`"env:$($_.Name)\`" \`"$($_.Value -replace '"', '\"\"')\`"; " })
    $(Get-ChildItem function:bar | ForEach-Object { "`$function:$($_.Name) = \`"$($_.Definition -replace '"', '\"\"')\`"; " })
    $(Get-ChildItem alias:baz | ForEach-Object { "`$alias:$($_.Name) = \`"$($_.Definition)\`"; " })
    "@
    

    Important:

    • I've omitted the call to Windows Terminal (wt.exe), as that would create another PowerShell session, which means that only the following definitions would be preserved for that session:

      • Environment variables.
      • The current location (working directory), IF its default shell is configured to use the parent process' working directory. Alternatively, and more predictably, pass the working dir. explicitly with the -d option:
        wt.exe -d \"$((Get-Location -PSProvider FileSystem).ProviderPath)\"
      • If that is enough, you can remove the commands that preserve aliases, functions, and regular variables, add -WindowStyle Hidden to Start-Process, remove -NoExit before -Command in the argument list, and add a wt.exe call at the bottom.
    • Preserving the other types of definitions requires working directly in the elevated powershell session, which will invariably use a regular (conhost.exe) console window, however.

    In general, it's best to place definitions that should be available in both regular and elevated sessions in your $PROFILE file.

    Complementarily, see this answer for convenience function Enter-AdminPSSession, which allows you to pass a script block to execute in the elevated session, to which you can pass values from the caller's state as arguments.

    Note:

    • The above uses the Windows PowerShell CLI, powershell.exe. To use PowerShell (Core) 7+ instead, substitute pwsh.exe.

    • The above covers preserving the current file-system location, environment variables, aliases, and functions in a generic fashion.

    • Caveat: By contrast, preserving regular variables is limited to strings and numbers - in essence, instances of those data types whose stringified representation is recognized as such when interpreted as a source-code literal.

      • With nontrivial additional effort, supporting more data types is possible, by using Base64 encoding with the CLI's -EncodedCommand and -EncodedArguments parameters as shown in this answer, but the range of types that can be represented with type fidelity is fundamentally limited by PowerShell's XML-based serialization infrastructure - see this answer.

    [1] This limitation also surfaces in PowerShell (Core) 7, where Start-Process now has an -Environment parameter for modifying the inherited environment, which, however, can not be combined with -Verb RunAs. Unfortunately, as of v7.5.0, such an invalid parameter combination is quietly ignored instead of reporting an error (which is what happens in an analogous .NET API call): see GitHub issue #20923.