Enter-PSSession opens a shell on another machine and leaves you there. The prompt changes to the remote computer name, every command you type after that runs on the far end, and you stay there until you type exit. It is the most direct thing WinRM gives you, and it is also the point at which a lot of working local knowledge quietly stops applying.
The reason it stops applying is that a remote session is a different process on a different machine, reached over a protocol that can only carry text. Objects are copied rather than passed. Variables from your prompt do not exist on the other side. Errors that would stop a local script do not stop a remote one. None of that is a bug and all of it is documented, but it is spread across nine separate reference and conceptual pages, every one of which is linked at the end of this article.
This article collects the five boundaries where a remote session behaves differently from the local shell, each one with the documented sentence that governs it and, where it could be measured, a measurement. Several of the examples below were run in the sandbox against PowerShell 7.4.6 using Start-Job rather than a real remote host, because the documentation states that background jobs use the same out-of-process serialization as remoting. Where that is what was done, the text says so.
Applies to: Windows 10 / 11 and Windows Server 2016 / 2019 / 2022, Windows PowerShell 5.1 and PowerShell 7
Quick answer
Three commands: enable the listener on the target, prove it answers from the client, then connect. Run the first one in the shell you actually want to answer the call, because that choice decides which PowerShell you land in later.
# On SRV-PROD-01, from an elevated prompt.
# Run this inside Windows PowerShell to configure the Microsoft.PowerShell endpoint,
# or inside pwsh to configure a PowerShell.7 endpoint. They are separate.
Enable-PSRemoting -Force
# From your workstation: does the WinRM service answer at all?
# This is an identification request, not a logon, so it fails early and cheaply.
Test-WSMan -ComputerName SRV-PROD-01
# Interactive session. The prompt changes and stays changed until you type exit.
Enter-PSSession -ComputerName SRV-PROD-01
A successful connection looks like this. Everything typed after the bracketed name runs on the remote machine, including redirection, so a file written here is written there.
PS C:\> Enter-PSSession -ComputerName SRV-PROD-01
[SRV-PROD-01]: PS C:\>
[SRV-PROD-01]: PS C:\> exit
PS C:\>
Enter-PSSession reference. If the connection fails instead, the four error strings in Boundary five further down cover the documented first-time failures.
What Enter-PSSession does
On Windows, PowerShell remoting runs over WinRM, Microsoft’s implementation of the WS-Management protocol. The client opens an HTTP connection to TCP 5985 on the target, or TCP 5986 for HTTPS, authenticates, and asks the WinRM service to start a PowerShell process for it. The Microsoft port reference gives those two numbers for WinRM 2.0 and notes that WinRM 1.1 and earlier used TCP 80 and TCP 443 instead, which is why an old firewall rule sometimes points at the wrong port.
Three cmdlets use that connection in three different shapes. Picking the wrong one is a structural mistake that is hard to see once the script is written.
| Cmdlet | Shape | The connection lives for |
|---|---|---|
Enter-PSSession | One machine, interactive. You type, it runs there. | Until you type exit or run Exit-PSSession |
Invoke-Command -ComputerName | Many machines, one script block, in parallel. | One command, then it is torn down |
New-PSSession | Many machines, reusable, holds state between commands. | Until Remove-PSSession or the shell closes |
The interactive one has a documented restriction that catches people who try to script it. The Enter-PSSession reference states it as an IMPORTANT box:
Enter-PSSession is designed to substitute the current interactive session with a new interactive remote session. You shouldn’t call it from within a function or script or by passing it as a command to the pwsh executable.”
The same page draws a second distinction that matters when a command contains anything the local parser might touch: unlike Invoke-Command, which parses and interprets the commands before it sends them, Enter-PSSession sends the commands directly to the remote computer without interpretation.
Three remote paths, and only one is WinRM
A large number of cmdlets and command-line tools accept a remote machine name and do not go anywhere near WinRM. They look identical at the prompt and they fail in completely different ways, which is why “remoting is broken” is rarely a single problem. The about_Remote_Requirements page says this in a note at the top:
| Path | What uses it | Ports | What the failure reads like |
|---|---|---|---|
| RPC / DCOM | -ComputerName on the older cmdlets, systeminfo /s, sc \\SRV-PROD-01 | TCP 135, then a dynamic port in 49152-65535 | The RPC server is unavailable |
| WinRM / WS-Management | Enter-PSSession, Invoke-Command, New-PSSession, Get-CimInstance | TCP 5985, or 5986 for HTTPS | The WinRM client cannot process the request |
| Neither | The hypervisor channel, for example Invoke-VMScript through VMware Tools | None on the guest network | VMware Tools is not running |
The dynamic range is the reason the two paths do not fail together. The Microsoft port requirements page gives TCP 135 as the RPC Endpoint Mapper and 49152 to 65535 as the default dynamic range on Windows Server 2008 and later. A firewall that allows 5985 and nothing else lets Invoke-Command through and blocks systeminfo /s, on the same machine, in the same second.
There is a reliable way to tell which path a cmdlet is on, and it is not the parameter name. The about_Remote_Requirements page says to read the description of the -ComputerName parameter on the cmdlet itself: a cmdlet that uses PowerShell remoting says so there, and one that does not, does not. Reading Windows event logs with PowerShell is the worked example already on this site: its -ComputerName parameter uses RPC, not WinRM.
Get-WinEvent -ComputerName SRV-PROD-01 works. It proves RPC works. It says nothing at all about WinRM, and the reverse is equally true.
Before the first example
Four things, and the fourth is the one people skip. Every example below uses the same two machines: a workstation you are sitting at, and a target called SRV-PROD-01.
1. An elevated prompt on the target. Enable-PSRemoting starts a listener and edits a firewall rule, so it needs administrator rights. The about_Remote_Requirements page lists loopback connections, session configuration management and any change under the LocalHost node of the WSMan: drive as operations that require starting PowerShell with Run as administrator even for a member of the Administrators group.
# On SRV-PROD-01. Windows Server 2012 and later ship with remoting already enabled,
# so this is usually a repair rather than a first-time setup.
Enable-PSRemoting -Force
2. Membership on the target, not on your workstation. The account you connect with has to be allowed by the session configuration at the far end. Two documented statements sit close together here and they are worth reading in order, because they are not saying the same thing.
about_Remote_Requirements page says to connect “with a user account that’s a member of the Administrators or Remote Management Users group on the remote computer”, and then says the security descriptors on the default configurations Microsoft.PowerShell and Microsoft.PowerShell32 “only allow access to members of the Administrators group”. Both are true: the second describes the two Windows PowerShell defaults, the first describes what a session configuration is allowed to grant. The Get-PSSessionConfiguration output printed on the Enable-PSRemoting page settles it for the PowerShell 7 endpoints, which list BUILTIN\Remote Management Users AccessAllowed.
3. A way to test the port that is not the cmdlet itself. When a connection fails you want to know whether you are looking at a network problem or an authentication problem, and the remoting cmdlets do not separate those cleanly. Test-NetConnection answers the first half.
# Is the listener reachable at all? This is a TCP test and nothing more:
# it does not authenticate, so a success here still tells you nothing about credentials.
Test-NetConnection -ComputerName SRV-PROD-01 -Port 5985
4. Know which PowerShell you are running, on both ends. Capture it now, before anything is connected, because the first boundary below turns on exactly this.
# Run this at your own prompt and write the answer down.
# Windows PowerShell reports 5.1.x; pwsh reports 7.x.
$PSVersionTable.PSVersion
$PSVersionTable.PSEdition
Practical examples
1. Prove the listener answers before you trust it
The problem: A connection attempt hangs and then returns a paragraph of text listing several possible causes at once. You want to know which half of the problem you have before you start reading it.
The solution: Test-WSMan sends an identification request, which the WinRM service answers without a logon. If it comes back, the service is running and reachable; if it does not, nothing about credentials matters yet.
# An identification request, not a session. It is the cheapest possible probe.
Test-WSMan -ComputerName SRV-PROD-01
# Adding -Authentication makes it a real logon, and the reference page notes that
# this is what makes the cmdlet return the operating system version as well.
Test-WSMan -ComputerName SRV-PROD-01 -Authentication Default
Test-WSMan reference page describes the output in prose, naming the identity schema, the protocol version, the product vendor and the product version, but publishes no sample output block, so none is invented here. Run it once against a machine you know is healthy and keep that shape as your reference.
2. One command, many machines
The problem: You need the same answer from thirty servers and you do not want thirty sessions.
The solution: Invoke-Command takes a list. It connects in the order given, runs the script block everywhere in parallel, and tags every returned object with the machine it came from.
# -ComputerName takes an array. The default -ThrottleLimit is 32 concurrent
# connections, so a list longer than that queues rather than failing.
Invoke-Command -ComputerName SRV-PROD-01, SRV-PROD-02 -ScriptBlock { Get-Culture }
The PSComputerName column is added by the remoting layer, not by the command inside the script block. It is what makes the combined output usable. The block below is the capture published on the about_Remote_Output page rather than a re-run, so the machine names in it are Microsoft’s Server01 and Server02 and not the pair used elsewhere in this article.
LCID Name DisplayName PSComputerName
---- ---- ----------- --------------
1033 en-US English (United States) Server01
1033 es-AR Spanish (Argentina) Server02
PSComputerName interleaves rows from different machines, which reads as noise in a report.
3. An interactive session for a fix you cannot script yet
The problem: One server is behaving oddly and you do not yet know what you are going to type next. A script block is the wrong tool because you are still exploring.
The solution: Enter-PSSession puts you on the machine. This is the case the cmdlet exists for, and the one place where its restriction on scripting does not matter.
# The prompt changes to [SRV-PROD-01] and stays changed.
Enter-PSSession -ComputerName SRV-PROD-01
# Everything below this line is typed inside that session.
# Redirection is resolved on the far end, so this file lands on SRV-PROD-01.
New-Item -Path C:\bat -ItemType Directory -Force | Out-Null
Get-Service -Name Spooler | Format-List * > C:\bat\spooler.txt
# exit and Exit-PSSession do the same thing.
exit
Enter-PSSession reference makes the point with a worked example: after the session ends, a Get-ChildItem on the local machine cannot find the file, because it was never written there.
4. A session that survives more than one command
The problem: A three-step job where step two depends on a variable set in step one. Three separate Invoke-Command calls give you three separate processes and nothing carries over.
The solution: New-PSSession creates a persistent session. State set inside it is still there on the next call, because it is the same process on the far end.
# One connection, reused. $s holds a PSSession object, not a machine name.
$s = New-PSSession -ComputerName SRV-PROD-01
# Step one defines a variable INSIDE the remote session.
Invoke-Command -Session $s -ScriptBlock { $stopped = Get-Service | Where-Object Status -eq 'Stopped' }
# Step two still sees it, because it is the same remote process.
Invoke-Command -Session $s -ScriptBlock { $stopped.Count }
A freshly created session prints this shape. The ConfigurationName column is the one to read: it is the endpoint you landed on, and the next section is about why it matters. This is the verification example published on the about_Remote_Requirements page, which is why it shows a loopback connection to localhost rather than to SRV-PROD-01.
Id Name ComputerName State ConfigurationName
-- ---- ------------ ----- -----------------
1 Session1 localhost Opened Microsoft.PowerShell
5. Take it all down again
The problem: Sessions and listeners are both things you created, and both outlive the work. A forgotten PSSession holds a process open on the server; a listener enabled for one afternoon is a permanent inbound service.
The solution: Remove the sessions, then confirm with a command that should now print nothing at all.
# Close every session this shell owns. This is the undo for example 4.
Get-PSSession | Remove-PSSession
# The pass condition is an empty result. It looks like a failure and it is not.
Get-PSSession
# If the listener was enabled only for this job, take it away again on SRV-PROD-01.
# Disable-PSRemoting stops new connections; the documentation for it is worth reading
# first, because it does not undo every change Enable-PSRemoting made.
Disable-PSRemoting -Force
Get-PSSession printing nothing is the confirmation. An empty result here means every session is closed, not that the command failed.
Boundary one: which PowerShell answers
Install PowerShell 7 on a server, run Enable-PSRemoting from it, connect with Enter-PSSession -ComputerName SRV-PROD-01 from your workstation, and check the version on the far side. It reports Windows PowerShell 5.1. Nothing failed and nothing warned you at connect time, because the two halves of this are documented on two different pages and neither one mentions the other.
The Enable-PSRemoting page explains why in its description. Running the cmdlet configures an endpoint for the installation it was run from, and it creates two names for that endpoint: a short one and a version-specific one.
Enable-PSRemoting in PowerShell 6.2, you will get two configured endpoints named PowerShell.6, PowerShell.6.2.2.”
The cmdlet says so when it runs, too. This warning is the output block published in its first two examples, and it is the only notice you get.
WARNING: PowerShell remoting has been enabled only for PowerShell Core configurations and
does not affect Windows PowerShell remoting configurations. Run this cmdlet in Windows
PowerShell to affect all PowerShell remoting configurations.
The other half of the trap is on the client. The -ConfigurationName parameter has a default, and the default is not the newest endpoint on the target: both the Enter-PSSession and the Invoke-Command pages state that it is the value of the $PSSessionConfigurationName preference variable, and that when that variable is not set the default is Microsoft.PowerShell.
# Ask the target what it actually offers. Run this in a session, or locally on the server.
Get-PSSessionConfiguration | Select-Object Name, PSVersion
# Then ask for the one you want by name.
Enter-PSSession -ComputerName SRV-PROD-01 -ConfigurationName PowerShell.7
# Or set the default once per shell, so every later connection follows it.
$PSSessionConfigurationName = 'PowerShell.7'
One entry from that list looks like the block below, which is the shape published in the fourth example on the Enable-PSRemoting page. The version numbers there are from the build Microsoft captured it on, so read the names rather than the digits.
Name : PowerShell.7
PSVersion : 7.3
StartupScript :
RunAsUser :
Permission : NT AUTHORITY\INTERACTIVE AccessAllowed,
BUILTIN\Administrators AccessAllowed,
BUILTIN\Remote Management Users AccessAllowed
pwsh on your workstation and assuming the far end is also PowerShell 7. The client version has no effect on the endpoint you land in. The only honest check is Invoke-Command -Session $s -ScriptBlock { $PSVersionTable.PSVersion } before you rely on anything newer than 5.1.
Boundary two: what comes back is a copy
A remote command returns objects that look right, print right, sort right, and cannot be acted on. The about_Remote_Output page gives the mechanism in one paragraph: live .NET objects cannot cross a network, so they are serialized to XML, sent, and rebuilt on your machine as a snapshot.
about_Remote_Variables page adds the marker to look for: the pstypenames property “contains the original type name prefixed with Deserialized”.
That is measurable without a remote host, because [System.Management.Automation.PSSerializer] is the same serializer. The sandbox ran a live Get-Process object through a serialize and deserialize round trip on PowerShell 7.4.6 and counted what survived.
# Measured on PowerShell 7.4.6. This is the same serializer remoting uses,
# so the result is the same shape an object has after crossing the wire.
$p = Get-Process -Id $PID
$xml = [System.Management.Automation.PSSerializer]::Serialize($p)
$d = [System.Management.Automation.PSSerializer]::Deserialize($xml)
$p.pstypenames[0]
$d.pstypenames[0]
@($p | Get-Member -MemberType Method).Count
@($d | Get-Member -MemberType Method).Count
System.Diagnostics.Process
Deserialized.System.Diagnostics.Process
19
2
Nineteen methods became two, GetType and ToString. Sixty-nine properties became sixty-three, so every value you were reading is still there. That is exactly why the failure is late: the object inspects perfectly and only breaks when you try to make it do something.
Method invocation failed because [Deserialized.System.Diagnostics.Process] does not contain a method named 'Kill'.
Not everything is flattened. The same page says a limited set of types are rehydrated back to the original type, and the round trip confirms which: System.String, System.Int32, System.Version, System.Guid, System.DateTime and System.TimeSpan all came back as themselves, with no Deserialized prefix. The page also warns that the copy is imperfect for complex types, and gives the example that rehydrated certificate objects do not include the private key.
The fix is to do the acting on the far side and bring back only what you need to read.
# Wrong: the object arrives with its methods stripped and .Kill() fails locally.
$p = Invoke-Command -ComputerName SRV-PROD-01 -ScriptBlock { Get-Process -Name tomcat9 }
$p.Kill()
# Right: act where the object is alive, and return a plain result you only need to read.
Invoke-Command -ComputerName SRV-PROD-01 -ScriptBlock {
Stop-Process -Name tomcat9 -Force
[pscustomobject]@{ Host = $env:COMPUTERNAME; Stopped = -not (Get-Process -Name tomcat9 -ErrorAction SilentlyContinue) }
}
Boundary three: local variables do not travel
A script block handed to Invoke-Command is sent as text and evaluated on the other machine. Any variable in it is looked up there. The about_Remote_Variables page states the rule without qualification: PowerShell assumes the variables used in remote commands are defined in the session in which the command runs.
What makes this one costly is the shape of the failure rather than its size. The sandbox ran the three forms against Start-Job, which the same page names as using the identical out-of-process mechanism, on PowerShell 7.4.6.
# Measured with Start-Job on PowerShell 7.4.6, standing in for a remote session.
$server = 'SRV-PROD-01'
Start-Job { 'bare : [' + $server + ']' } | Receive-Job -Wait -AutoRemoveJob
Start-Job { 'using : [' + $using:server + ']' } | Receive-Job -Wait -AutoRemoveJob
Start-Job { param($s) 'argl : [' + $s + ']' } -ArgumentList $server | Receive-Job -Wait -AutoRemoveJob
bare : []
using : [SRV-PROD-01]
argl : [SRV-PROD-01]
The first line is the whole problem. The variable is not an error, it is empty. A second measurement confirmed that setting $ErrorActionPreference = 'Stop' inside the job changes nothing: the variable is simply $null and no error is raised.
Get-Service -Name $svc inside a remote script block, where $svc is a local variable. It does not fail. It runs as Get-Service with no filter and returns every service on the machine, which in a pipeline that stops or disables things is the worst possible kind of success.
The $using: scope modifier, available since PowerShell 3.0, is the fix. It has one documented limitation worth knowing before you reach for it, and the sandbox found that the limitation is enforced by the parser rather than at run time.
# The documented syntax. $using:<name> expands to the caller's value.
$svc = 'Spooler'
Invoke-Command -ComputerName SRV-PROD-01 -ScriptBlock { Get-Service -Name $using:svc }
# Splatting works too, with the @ symbol in front of the modifier.
$splat = @{ Name = 'Win*'; Include = 'WinRM' }
Invoke-Command -Session $s -ScriptBlock { Get-Service @using:splat }
Assigning to it does not. The about_Remote_Variables page says the modifier cannot be used to modify a local variable inside the session; parsing that line in the sandbox shows the rejection is not subtle.
ParserError: The assignment expression is not valid. The input to an assignment operator
must be an object that is able to accept assignments, such as a variable or a property.
ErrorId: InvalidLeftHandSide
One more documented detail decides whether $using: is safe in a loop. For remote and out-of-process sessions the embedded values are always independent copies; for thread sessions, meaning Start-ThreadJob and ForEach-Object -Parallel, they are passed by reference. The same line of code has different sharing semantics depending on where you run it.
Boundary four: errors change class at the wire
A remote command that fails hard does not stop your script. The Invoke-Command NOTES section states the rule and the reason:
The last sentence is the one that catches people. It applies to a single target as well, so a try/catch wrapped around a one-machine call does not behave the way the same code behaves locally. The sandbox measured the boundary with a job, which sits on the same out-of-process path.
# Measured on PowerShell 7.4.6. A throw inside the out-of-process script block
# does NOT reach the catch block around the receiving call.
$caught = 'no'
try {
$j = Start-Job -ScriptBlock { throw 'boom2' }
$null = Receive-Job -Job $j -Wait -AutoRemoveJob
} catch {
$caught = 'yes: ' + $_.Exception.Message
}
$caught
OperationStopped: boom2
no
The error text is printed, the catch block never runs, and $caught stays no. The script continues past a step that did not happen. The measurement also confirmed the other half: the statement after the throw inside the block did not run, so the error is still terminating on the far side and only changes class on the way back.
The fix is to check rather than to catch. Return a result you can test, or keep the error handling on the remote side where the error is still terminating.
# Option one: handle it where it is still a terminating error.
Invoke-Command -ComputerName SRV-PROD-01 -ScriptBlock {
try { Restart-Service -Name Spooler -ErrorAction Stop; 'restarted' }
catch { 'failed: ' + $_.Exception.Message }
}
# Option two: collect the errors explicitly instead of relying on try/catch.
# C:\bat\missing.txt is deliberately absent, so every target produces one error record.
Invoke-Command -ComputerName SRV-PROD-01, SRV-PROD-02 -ScriptBlock { Get-Content C:\bat\missing.txt } -ErrorVariable remoteErrors
$remoteErrors | ForEach-Object { $_.Exception.Message }
# The records carry origin information too, but which properties are populated varies.
# Read your own before scripting against them.
$remoteErrors[0] | Get-Member
Boundary five: when the connection is refused
These four error strings are the ones the documentation itself singles out. All four are quoted from the about_Remote_Troubleshooting page, which is the authored source for each of them.
| What you see | What it usually means | Where to look |
|---|---|---|
| ERROR: Access is denied. You need to run this cmdlet from an elevated process. | The local prompt is not elevated. | Start PowerShell with Run as administrator |
| ERROR: The connection to the remote host was refused. Verify that the WS-Management service is running on the remote host… | No listener on the target, or the wrong port. | Enable-PSRemoting on the target, then Test-WSMan |
| ERROR: ACCESS IS DENIED | An account from another domain that is an administrator on the target, arriving with a standard-user privilege token. | The LocalAccountTokenFilterPolicy value |
| ERROR: The WinRM client cannot process the request… added to the TrustedHosts configuration setting. | An IP address, or a workgroup machine on either end. | The TrustedHosts list, or HTTPS |
The fourth is the most common and the least obvious, because nothing in the command mentions authentication. The about_Remote_Troubleshooting page explains it: Kerberos authentication does not support IP addresses, so specifying one forces NTLM, and NTLM then requires either HTTPS transport or an entry in TrustedHosts.
# Read the current list first. This is a machine-wide setting, not a per-user one.
Get-Item WSMan:\localhost\Client\TrustedHosts
# Append rather than replace, or you will quietly remove somebody else's entry.
$newServer = 'SRV-PROD-01.corp.example.com'
$curValue = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "$curValue, $newServer"
TrustedHosts affects all users of the computer. A wildcard entry of * is shown in the documentation as valid syntax and is a poor choice on a machine anyone else uses.
The third row is the same UAC token filtering behaviour covered in runas and UAC token filtering, seen from the network side. The remoting page documents the LocalAccountTokenFilterPolicy registry value as the switch that changes it, and carries a caution that it disables UAC remote restrictions for all users of the affected machine. Read that article before setting it: the reason the token arrives filtered is the interesting part, and it decides whether the registry change is the right fix at all.
One more failure has nothing to do with the network. The Enter-PSSession cmdlet requires Get-Command, Out-Default and Exit-PSSession to be present in the remote session configuration, and its reference page states that the command fails if they are not, so a constrained endpoint that leaves them out refuses an interactive session even while ordinary Invoke-Command calls against it keep working.
Hidden gems
Testing locally hides every boundary in this article. Invoke-Command with no target runs in your own process, so nothing is serialized. The sandbox measured both: the local call returned a live System.Diagnostics.Process with 19 methods, and the same command through an out-of-process job returned Deserialized.System.Diagnostics.Process with two. A script that works perfectly in local testing can fail on its first real target for reasons the test could not produce.
-Con is ambiguous and -Com is not. PowerShell resolves a parameter name by unambiguous prefix, and Enter-PSSession has a crowd of parameters starting with Con. Binding them in the sandbox on PowerShell 7.4.6 gives the exact message:
Parameter cannot be processed because the parameter name 'Con' is ambiguous.
Possible matches include: -ConnectionUri -ContainerId -ConfigurationName -ConnectingTimeout.
-Com resolves cleanly to -ComputerName because nothing else on the cmdlet starts with those three letters. The list of matches is shorter on Windows PowerShell 5.1, because -ConnectingTimeout was introduced in PowerShell 7.2 and the 5.1 syntax has no SSH parameter set at all, but -Con is still ambiguous there.
The default throttle is 32. The -ThrottleLimit parameter of Invoke-Command defaults to 32 concurrent connections, and the reference page notes the limit applies to the current command only, not to the session or the computer. Two scripts running side by side each get their own 32.
A disconnected session is not necessarily free. The Invoke-Command NOTES section warns that the State property is relative to the current session: Disconnected means it is not connected to your session, not that nobody has it. The property to read is Availability, where None means you can connect and Busy means somebody else already did.
Two reference pages describe the same default differently. The -ConfigurationName parameter is shared by Enter-PSSession and Invoke-Command. The Enter-PSSession page prints its default as None in the parameter table while its own prose three lines above says the default is Microsoft.PowerShell; the Invoke-Command page prints the full sentence in the table instead. The prose is the part to trust: the generated table on the Enter-PSSession page is describing the parameter attribute, not the behaviour you actually get.
Cross-shell equivalent
WinRM is not only a PowerShell feature, and PowerShell remoting is no longer only WinRM. Three commands, three destinations, two transports, and only the second one is PowerShell remoting as this article has described it.
| Where you are | The command | Transport |
|---|---|---|
| CMD, or a batch file | winrs -r:SRV-PROD-01 ipconfig /all | WinRM, same listener and same port |
| PowerShell 7 to a host running sshd | Enter-PSSession -HostName admin@SRV-PROD-01 | SSH |
| Any shell, to a Linux host | ssh admin@SRV-LINUX-01 | SSH, no PowerShell involved |
The SSH path is a genuine version boundary rather than a preference. The -HostName parameter set was added in PowerShell 6.0, so it does not exist in Windows PowerShell 5.1 at all. The reference page is direct about what you give up with it: WinRM based quotas, session options, custom endpoint configuration, and disconnect and reconnect features are currently not supported over SSH.
# PowerShell 7 and later only. The remote host needs sshd configured with a
# PowerShell subsystem; the default subsystem name is powershell.
Enter-PSSession -HostName admin@SRV-PROD-01
# The same cmdlet, the WinRM path, on either version.
Enter-PSSession -ComputerName SRV-PROD-01
Enter-PSSession and Enter-PSHostProcess to work from within any interactive remote session. The SSH and OpenSSH cheat sheet covers the client side of the SSH half.
Where this matters
A patch window across thirty servers. One Invoke-Command with a script block connects to them in parallel up to the throttle limit instead of one at a time, and the PSComputerName column tells you which host each line came from.
A service that will not start on one machine. Enter-PSSession puts you on the box with your profile on that machine loaded, which beats guessing a script block for a problem you have not diagnosed yet.
A migration script that worked in the lab. The lab was one machine and every Invoke-Command ran locally, so nothing was ever serialized and every local variable resolved. The first real target is where both assumptions break.
A new server that refuses every connection. Checking 5985 with the Port Checker and then running Test-WSMan separates a firewall problem from a credentials problem before you start changing anything.
A workgroup appliance or a DMZ host. No domain means no Kerberos, which means TrustedHosts or HTTPS on both ends, and a documented requirement to pass -Credential even when connecting as yourself.
A virtual machine that lost its network. None of this applies, because remoting needs the guest network to be working. That is the case Invoke-VMScript through VMware Tools exists for.
Tips and limitations
- Windows Server 2012 and later ship with PowerShell remoting enabled by default. On those machines
Enable-PSRemotingis a repair, not a first-time setup. - On client versions of Windows,
Enable-PSRemotingfails by default on a public network profile. The documented workaround is the-SkipNetworkProfileCheckparameter, which creates a rule allowing access from the same local subnet only. - You can have only one interactive session at a time.
Enter-PSSessionreplaces your shell rather than opening a second one. - The
Enter-PSSessionpage states that you cannot useDisconnect-PSSession,Connect-PSSessionorReceive-PSSessionto disconnect from or reconnect to an interactive session. A session you entered is not one you can walk away from and pick up later. - Your remote user profile runs when you enter a session, including anything in it that changes the prompt or imports modules. A slow profile is a slow connection.
Enable-PSRemotingandTest-WSManare Windows only. Both reference pages carry the note that the cmdlet is available on the Windows platform only.- The remoting cmdlets themselves are not Windows only in PowerShell 7: the SSH parameter sets work from Linux and macOS, but the
-ComputerNameset needs a WSMan client library and fails without one. - Adding a machine to
TrustedHostschanges the setting for every user of that computer, and the documentation carries an explicit caution about it.
Official documentation
- Enter-PSSession: PowerShell | Microsoft Learn
- Invoke-Command: PowerShell | Microsoft Learn
- New-PSSession: PowerShell | Microsoft Learn
- Enable-PSRemoting: PowerShell | Microsoft Learn
- Test-WSMan: PowerShell | Microsoft Learn
- about_Remote_Requirements | Microsoft Learn
- about_Remote_Variables | Microsoft Learn
- about_Remote_Output | Microsoft Learn
- about_Remote_Troubleshooting | Microsoft Learn
- Service overview and network port requirements | Microsoft Learn
Related tools
- Port Checker: check whether 5985 or 5986 is open before spending time on credentials, since a refused connection and a rejected logon read almost the same.
Related guides
- Reading and clearing Windows event logs with PowerShell: the worked example of a
-ComputerNameparameter that uses RPC rather than WinRM. - Set-Service StartupType and Get-Service in PowerShell: the service cmdlets lost
-ComputerNamein PowerShell 6.0, which is why they now need a session. - Test-NetConnection in Windows: the port test that tells you whether the listener is reachable at all.
- runas and UAC token filtering: why an administrator account can arrive at the far end holding a standard-user token.
- Reading your access token with whoami: how to check which identity and which privileges you actually landed with inside the session.
- Running a script inside a VM with Invoke-VMScript: the path that needs no WinRM, no SSH and no guest network at all.
- Common network ports cheat sheet: 5985 and 5986 in context with the RPC range and everything else you may be blocking.
Five cheat sheets, one PDF
Subnet masks, PowerShell, Linux commands, HTTP status codes and the ESXi command line - one page each, free to keep. Leave an address and it arrives in a minute.