Cheat Sheets

PowerShell Cheat Sheet: Cmdlets, Remoting and Modules (PDF)

PowerShell 7 runs the same object pipeline on Windows, Linux and macOS, so one PowerShell cheat sheet can cover all three. The commands do not always behave the same, though. Get-Service does not exist on Linux, ls is not an alias there, and Invoke-Command -ComputerName lands you in Windows PowerShell 5.1 unless you ask for 7.

Original content from computingforgeeks.com - post 179590

The reference tables for cmdlets, the pipeline, objects, remoting and modules come first, then the parts that need explaining, such as SSH remoting from Linux into Windows. We checked the commands in October 2026 on PowerShell 7.6.9 on Windows Server 2025 and Ubuntu 26.04, and every output shown comes from one of those two machines.

PowerShell cheat sheet: quick reference

Each table below groups the commands by task. Anything marked Windows only needs Windows APIs and fails on Linux and macOS. The rest is cross-platform, and we ran most rows on the Ubuntu box and the Windows-specific ones on Windows Server; a few, such as Enter-PSSession, are interactive forms of commands we ran non-interactively.

Start PowerShell and get help

CommandWhat it does
pwshStart PowerShell 7 (Windows PowerShell 5.1 is powershell.exe)
pwsh -NoProfile -c 'Get-Date'Run one command without loading the profile
$PSVersionTableVersion, edition (Core or Desktop) and OS
Get-Command -Noun ProcessFind cmdlets by noun
Get-Command *-ServiceFind cmdlets by wildcard
Get-Help Get-Process -ExamplesUsage examples for a cmdlet
Update-Help -UICulture en-USDownload full help files
Get-Process | Get-MemberProperties and methods of the output objects
Get-Alias -Definition Get-ChildItemAliases that point to a cmdlet

Pipeline and objects

CommandWhat it does
Get-Process | Where-Object CPU -gt 1Filter objects by a property
Select-Object -First 5 Name, IdKeep some properties and some objects
Sort-Object WorkingSet64 -DescendingSort by a property
Group-Object ExtensionCount objects per value
Measure-Object Length -Sum -AverageCount, sum, average, max
ForEach-Object { $_.Name }Run a block for each object
ForEach-Object -Parallel { ... } -ThrottleLimit 5Run blocks in parallel runspaces
Format-Table Name, Id -AutoSizeTable view (use last in the pipeline)
Format-List *Every property, one per line
@{n='MB';e={$_.WorkingSet64/1MB}}Calculated property for Select-Object

Files and text

CommandWhat it does
Get-ChildItem -Recurse -Filter *.confFind files recursively (alias gci, dir)
Get-Content app.log -Tail 20Last lines of a file (-Wait follows it)
Set-Content f.txt 'text'Write a file (overwrites)
Add-Content f.txt 'more'Append to a file
New-Item -ItemType Directory logsCreate a folder
Copy-Item src dst -RecurseCopy files or folders
Move-Item draft.txt final.txtMove or rename
Remove-Item old.bak -WhatIfPreview a delete before running it
Test-Path /etc/hostsTrue if the path exists
Select-String -Path *.log -Pattern 'error'grep for PowerShell

Processes, services and jobs

CommandWhat it does
Get-Process -Id $PIDThe current PowerShell process
Stop-Process -Name sleepKill processes by name
Start-Job { Start-Sleep 2; 'done' }Background job in a new process
Receive-Job -Id 1 -Wait -AutoRemoveJobCollect job output and clean up
Start-ThreadJob { ... }Lighter job in a thread
Get-Service | Where-Object Status -eq RunningRunning services (Windows only)
Restart-Service SpoolerRestart a service (Windows only)

Variables and operators

CommandWhat it does
-eq -ne -gt -lt -ge -leComparison (case-insensitive by default)
'pwsh' -like 'p*'Wildcard match
'error 404' -match '\d+'Regex match, result in $Matches[0]
1,2,3 -contains 2Is the value in the collection
'a,b,c' -split ','Split a string (-join reverses it)
$null ?? 'default'Null-coalescing
$x -gt 3 ? 'big' : 'small'Ternary operator
Test-Path f.txt && 'ok'Run the second command only on success (|| on failure)
Get-ChildItem @paramsSplat a hashtable of parameters
[pscustomobject]@{Name='web01'}Build your own object

JSON, CSV and web

CommandWhat it does
ConvertTo-Json -Depth 5Objects to JSON (default depth is 2)
Get-Content f.json | ConvertFrom-JsonJSON to objects
Export-Csv procs.csvObjects to CSV (no #TYPE line in 7)
Import-Csv procs.csvCSV rows to objects
Invoke-RestMethod https://api.github.com/...Call an API, JSON parsed for you
Invoke-WebRequest $url -Method HeadRaw HTTP request with status code

Errors and debugging

CommandWhat it does
try { ... -ErrorAction Stop } catch { $_ }Catch an error from one cmdlet
$ErrorActionPreference = 'Stop'Make every error terminating
-ErrorAction SilentlyContinueIgnore an error for one call
$Error[0].Exception.MessageMessage of the last error
Get-ErrorFull detail of the last error
Measure-Command { ... }Time a block

Remoting over SSH and WinRM

CommandWhat it does
Enter-PSSession -HostName web01 -UserName adminInteractive shell over SSH
Invoke-Command -HostName web01,db01 -UserName admin { hostname }Run a block on many hosts over SSH
$s = New-PSSession -HostName web01 -UserName adminReusable session
Get-PSSession | Remove-PSSessionList and close sessions
Invoke-Command -ComputerName srv01 -ConfigurationName PowerShell.7 { ... }WinRM into PowerShell 7 (Windows only)
Enable-PSRemoting -ForceRegister WinRM endpoints (Windows, admin)
Test-WSMan srv01Is WinRM answering

Modules with PSResourceGet

CommandWhat it does
Find-PSResource -Name PSScriptAnalyzerSearch the PowerShell Gallery
Install-PSResource Pester -Scope CurrentUser -TrustRepositoryInstall without the trust prompt
Get-InstalledPSResourceModules installed from a repository
Update-PSResource -Name PesterUpdate a module
Uninstall-PSResource -Name PesterRemove a module
Set-PSResourceRepository -Name PSGallery -TrustedTrust the gallery permanently
Get-Module -ListAvailableEvery module on the module path
Import-Module PSScriptAnalyzerLoad a module into the session

Windows system information

CommandWhat it does
Get-CimInstance Win32_OperatingSystemOS version, last boot, free memory
Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=3'Local disks and free space
Get-WinEvent -LogName System -MaxEvents 5Newest event log entries
Get-NetIPAddress -AddressFamily IPv4IP addresses per interface
Test-NetConnection 1.1.1.1 -Port 443TCP port check
Get-HotFixInstalled updates
Import-Module ServerManager -UseWindowsPowerShellLoad a 5.1-only module through a proxy session
Get-ExecutionPolicy -ListScript policy per scope

Profile and shell settings

CommandWhat it does
$PROFILE | Select-Object *All four profile paths
New-Item -ItemType File $PROFILE -ForceCreate your profile script
Get-PSReadLineOptionEdit mode, prediction and history file
Get-PSReadLineKeyHandler -Chord Ctrl+rWhat a key does (reverse history search)
$env:PSModulePath -split [IO.Path]::PathSeparatorWhere modules load from

That’s the whole sheet. The rest of this page explains the rows that behave differently between operating systems, with the output we got on both machines.

How do you install PowerShell 7 on Windows and Linux?

On Windows you install the MSI from the GitHub release, and on Ubuntu 26.04 you install the .deb from the same release page, because Microsoft’s apt repository for 26.04 does not carry the stable package yet. We registered the repository the documented way, ran apt-get update, and then asked for the stable package:

sudo apt-get install -y powershell

apt could not find it, even with the Microsoft source in place:

E: Unable to locate package powershell

The 26.04 feed (codename resolute) only lists powershell-preview, so Microsoft’s documented repository method fails on 26.04 even though Microsoft’s support page lists 26.04 as supported. The .deb from GitHub is the same universal package, and the version check below always picks the newest release instead of a pinned one:

VER=$(curl -s https://api.github.com/repos/PowerShell/PowerShell/releases/latest | grep '"tag_name"' | cut -d '"' -f 4 | sed 's/^v//')
echo "$VER"
wget https://github.com/PowerShell/PowerShell/releases/download/v${VER}/powershell_${VER}-1.deb_amd64.deb
sudo apt-get install -y ./powershell_${VER}-1.deb_amd64.deb

Then start pwsh and print the version table. A healthy install reports PSEdition Core and the distro name:

pwsh -NoProfile -c '$PSVersionTable'

Our Ubuntu box printed this table (we dropped the long PSCompatibleVersions row). The WSManStackVersion row shows up on Linux too, but WinRM remoting from Linux still fails, as the remoting section shows:

Name                           Value
----                           -----
PSVersion                      7.6.9
PSEdition                      Core
GitCommitId                    7.6.9
OS                             Ubuntu 26.04.1 LTS
Platform                       Unix
PSRemotingProtocolVersion      2.4
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

Whichever method you use, three quick checks confirm the install before you build anything on it:

  • which pwsh on Linux returns /usr/bin/pwsh, and on Windows the binary sits in C:\Program Files\PowerShell\7.
  • pwsh --version prints the same release the GitHub API reported.
  • $PSVersionTable.PSEdition reads Core, not Desktop.

On Ubuntu 24.04 the Microsoft repository does carry the stable package, and our PowerShell install guide for Ubuntu and Debian covers that path. Rocky Linux and RHEL readers can follow the PowerShell setup for RHEL 10 and Rocky Linux 10, and Mac users have the macOS install steps.

winget was absent on our Windows Server 2025 image (on Server, the Microsoft docs say it only ships with Desktop Experience), so we used the MSI. The properties come from Microsoft’s Windows install page; ENABLE_PSREMOTING=1 matters later because it registers the WinRM endpoint for PowerShell 7:

$ver = (Invoke-RestMethod https://api.github.com/repos/PowerShell/PowerShell/releases/latest).tag_name.TrimStart('v')
Invoke-WebRequest "https://github.com/PowerShell/PowerShell/releases/download/v$ver/PowerShell-$ver-win-x64.msi" -OutFile "C:\lab\PowerShell-$ver-win-x64.msi"
Start-Process msiexec.exe -Wait -ArgumentList "/package C:\lab\PowerShell-$ver-win-x64.msi /quiet ADD_EXPLORER_CONTEXT_MENU_OPENPOWERSHELL=1 ADD_FILE_CONTEXT_MENU_RUNPOWERSHELL=1 ENABLE_PSREMOTING=1 REGISTER_MANIFEST=1 USE_MU=1 ENABLE_MU=1 ADD_PATH=1"

The MSI installs to C:\Program Files\PowerShell\7 and leaves Windows PowerShell 5.1 in place, so both shells stay on the PATH. On Windows 11 or a Server with Desktop Experience, winget install --id Microsoft.PowerShell --source winget does the same job, and our winget guide covers that tool. The same Microsoft install page also says that winget installs the MSIX package by default since 7.6 and that no MSI ships from 7.7 onward, so scripted MSI installs like the one above have a limited life.

Here is how the two shells compared on the same Windows Server 2025 VM. We timed -NoProfile -c exit three times each:

CheckPowerShell 7 (pwsh)Windows PowerShell 5.1
Startup, three runs198 to 248 ms540 to 662 ms
(Get-Command).Count20522014
curlNot an aliasAlias of Invoke-WebRequest
&&, ??, ternaryWorkNot available
ForEach-Object -ParallelWorksNot available
Default WinRM endpoint it lands in5.1 unless you pass -ConfigurationName PowerShell.75.1

The startup gap is the one you feel. A scheduled task that runs a short script every minute pays about a third of a second less per run under pwsh. On Ubuntu, pwsh -NoProfile -c exit took 391 to 428 ms on a 2 vCPU VM.

How do you find the right cmdlet?

Use Get-Command to find the cmdlet, Get-Help to learn its parameters and Get-Member to see what objects it returns. Cmdlet names follow a Verb-Noun pattern, so a search by noun usually lists the whole family:

Get-Command -Noun Service

On Ubuntu that printed nothing at all, which is your first hint that the service cmdlets are Windows only. On Linux, (Get-Command).Count returned 298 commands against 2052 on Windows Server, because most Windows modules (networking, storage, CIM, event logs) have no Linux build.

Help is the other surprise on a fresh install. PowerShell 7 ships without full help files, so the first Get-Help call warns you and shows only the syntax:

Get-Help cannot find the Help files for this cmdlet on this computer. It is displaying only partial help.

Run Update-Help -UICulture en-US once per user. It took 45 seconds on our Ubuntu VM, and afterwards Get-Help Get-Process -Examples printed the full examples. The default scope is the current user, so each new account needs the step again; add -Scope AllUsers from an elevated or sudo shell to cover everyone on the machine.

Aliases are where cross-platform scripts break. On Windows, ls and cat are aliases of Get-ChildItem and Get-Content. On Linux, PowerShell removes those aliases so the real binaries keep working, and the alias lookup fails:

Get-Alias: This command cannot find a matching alias because an alias with the name 'ls' does not exist.

So ls | Select-Object Name works on Windows and returns nothing useful on Linux, because /usr/bin/ls sends text, not objects. Spell out the cmdlet name in any script that might run on both. We also tripped over a less obvious alias while scripting the lab: a helper function we named R never ran, because r is a built-in alias of Invoke-History and aliases win over functions. Every call failed with Cannot locate the history for command line. Check a short name with Get-Alias before you use it.

How does the PowerShell pipeline pass objects?

The PowerShell pipeline passes .NET objects, not text, so you filter and sort on named properties instead of cutting columns with awk. This command picks the five processes using the most memory and adds a calculated megabytes column:

Get-Process | Sort-Object WorkingSet64 -Descending | Select-Object -First 5 Name, Id, @{n='MB';e={[math]::Round($_.WorkingSet64/1MB,1)}}

On the Ubuntu VM pwsh itself was the largest process at about 110 MB. That figure matters if you plan to run pwsh in a small container or on a 512 MB VM, because the shell alone takes a fifth of the memory before your script loads anything:

Name                           Id     MB
----                           --     --
pwsh                        33466 110.50
snapd                       32569  43.10
unattended-upgrade-shutdown  1534  32.00
networkd-dispatcher          1441  29.20
packagekitd                 32450  22.20

Group-Object and Measure-Object answer the “how many” questions that would need sort | uniq -c in Bash. Grouping /etc by extension showed 45 files with no extension and 31 .conf files, and Measure-Object summed 1107 files in /usr/bin in one line:

Get-ChildItem /usr/bin | Measure-Object -Property Length -Sum -Average -Maximum

The result is itself an object, so you can pipe it on or read one property with .Sum:

Count             : 1107
Average           : 143424.717253839
Sum               : 158771162
Maximum           : 25214552
Minimum           :
StandardDeviation :
Property          : Length

ForEach-Object -Parallel is the PowerShell 7 feature that saves the most time in admin scripts, because each block runs in its own runspace. We timed five one-second sleeps both ways:

(Measure-Command { 1..5 | ForEach-Object { Start-Sleep 1 } }).TotalSeconds
(Measure-Command { 1..5 | ForEach-Object -Parallel { Start-Sleep 1 } -ThrottleLimit 5 }).TotalSeconds

Sequential took 5.01 seconds and parallel took 1.33 seconds on Ubuntu (1.08 on Windows Server). With -ThrottleLimit 2 over four items, Windows took 2.06 seconds, which shows the limit doing its job. Variables from the outer scope are not visible inside the parallel block, so pass them in as $using:name. The other limit is cost: parallel blocks run in separate runspaces, and the Microsoft docs warn that this overhead can make small, fast blocks slower than a plain loop. Use it for slow, independent work such as network calls and remote checks, not for tiny loops.

Keep Format-Table and Format-List at the very end of a pipeline. They turn objects into display records, so anything after them (Export-Csv, ConvertTo-Json) gets formatting data instead of your properties.

Which operators and variables do you use most?

The comparison operators are words (-eq, -gt, -like, -match) because > and < are redirection, and they are case-insensitive unless you use the c form such as -ceq. PowerShell 7 also adds the shorthand that 5.1 lacks. We checked each one on Ubuntu:

'error 404' -match '\d+'; $Matches[0]
$null ?? 'default'
$x = 5; $x -gt 3 ? 'big' : 'small'
Test-Path /etc/hosts && 'exists'
Get-Item /nope 2>$null || 'missing'

Each line printed what you would expect. Note that Test-Path printed its own True before exists, because && does not swallow the first command’s output:

True
404
default
big
True
exists
missing

Splatting keeps long commands readable. Put the parameters in a hashtable and pass it with @ instead of $; this counted 31 .conf files in /etc:

$p = @{Path='/etc'; Filter='*.conf'}
(Get-ChildItem @p).Count

When you need your own records, build them with [pscustomobject]@{Name='web01';Port=443}. Unlike a plain hashtable, the result prints as a table and works with Export-Csv and Sort-Object like any cmdlet output.

How do you work with JSON, CSV and REST APIs?

Invoke-RestMethod calls an API and turns the JSON reply into objects, so you can select fields straight away. This is how we checked the latest PowerShell release during the lab:

Invoke-RestMethod https://api.github.com/repos/PowerShell/PowerShell/releases/latest | Select-Object tag_name, published_at

The release tag and date come back as properties, so there is no jq step and no string parsing. That is also why the install section could read the latest version in one line on Windows, while the Bash version needs grep and cut:

tag_name published_at
-------- ------------
v7.6.9   10/25/2026 17:26:05

Going the other way, ConvertTo-Json has a trap: it stops at a depth of 2 and flattens anything deeper into a type name. PowerShell 7 at least warns you now:

@{a=@{b=@{c=@{d=1}}}} | ConvertTo-Json

Look at the c key. The data is gone and only the class name is left:

Resulting JSON is truncated as serialization has exceeded the set depth of 2.
{
  "a": {
    "b": {
      "c": "System.Collections.Hashtable"
    }
  }
}

Add -Depth 5 (or higher for API payloads) and the full tree comes back. We set it on every ConvertTo-Json that writes a config file or a request body, because a truncated payload usually fails far from the line that caused it.

CSV changed between versions too. Export-Csv in PowerShell 7 no longer writes the #TYPE header line that 5.1 adds, so the file starts with the column names and other tools read it cleanly. Our export of three processes began with "Name","Id", every value quoted, and Import-Csv read it back as objects.

How do you catch errors in PowerShell scripts?

Wrap the call in try and catch and add -ErrorAction Stop, because most cmdlet errors are non-terminating and skip the catch block otherwise. This is the pattern that works:

try { Get-Item /nope -ErrorAction Stop } catch { "Caught: $($_.Exception.Message)" }

The catch block ran and printed the clean message. Drop -ErrorAction Stop and the same line prints the red error instead, because the catch never fires for a non-terminating error:

Caught: Cannot find path '/nope' because it does not exist.

For a whole script, set $ErrorActionPreference = 'Stop' at the top instead of adding the parameter to every call. Inside the catch, $_.FullyQualifiedErrorId gives a stable ID such as PathNotFound,Microsoft.PowerShell.Commands.GetItemCommand, which is safer to test against than the message text. When you need the full picture after a failure, Get-Error prints the exception type, target object, script line and stack trace; on our test it ran to about 60 lines for a single missing path.

Which cmdlets only work on Windows?

Anything that talks to a Windows subsystem only works on Windows, because the cmdlets are thin wrappers over Windows APIs that have no Linux equivalent. Before you share a script with Linux users, check that it avoids these families or guards them with if ($IsWindows):

  • No service cmdlets (Get-Service, Restart-Service) run outside the guard.
  • No CIM or WMI queries (Get-CimInstance) run outside the guard.
  • No event log reads (Get-WinEvent) or registry paths (HKLM:) run outside the guard.
  • No networking cmdlets (Get-NetIPAddress, Test-NetConnection) or Get-HotFix run outside the guard.

On Linux these fail before they run, with the same “not recognized” error you get from a typo:

The term 'Get-Service' is not recognized as a name of a cmdlet, function, script file, or executable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.

On Windows Server 2025 the same cmdlets all worked under pwsh. This one returns the OS, last boot and free memory in a single object:

Get-CimInstance Win32_OperatingSystem | Select-Object Caption, Version, LastBootUpTime, @{n='FreeGB';e={[math]::Round($_.FreePhysicalMemory/1MB,2)}}

Our 8 GB lab VM had about 5 GB free right after boot. FreePhysicalMemory is reported in kilobytes, so dividing by 1MB turns it into gigabytes, which is the kind of unit detail a calculated property hides from whoever reads the report:

Caption                                  Version    LastBootUpTime        FreeGB
-------                                  -------    --------------        ------
Microsoft Windows Server 2025 Datacenter 10.0.26100 10/29/2026 8:59:25 AM   5.05

Some Windows modules still only load in Windows PowerShell 5.1. ServerManager is one, and Get-Module -ListAvailable ServerManager returns nothing in pwsh. Load it with -UseWindowsPowerShell and PowerShell 7 runs it in a hidden 5.1 process for you:

Import-Module ServerManager -UseWindowsPowerShell
Get-WindowsFeature | Where-Object Installed | Select-Object -First 5 Name

The import prints a warning that explains the trade-off. The objects you get back are deserialized copies, so their methods are gone and only the properties remain:

WARNING: Module ServerManager is loaded in Windows PowerShell using WinPSCompatSession remoting session; please note that all input and output of commands from this module will be deserialized objects. If you want to load this module into PowerShell please use 'Import-Module -SkipEditionCheck' syntax.

One result surprised us. Get-WmiObject was removed from PowerShell 7, yet on our Windows Server it still answered, and Get-WmiObject Win32_BIOS returned the VM’s firmware vendor. Get-Command showed why: it was a proxy function forwarding into the same WinPSCompatSession, not a native cmdlet. Scripts that lean on it run slower and get deserialized objects, so move them to Get-CimInstance, which is native in 7 and also reaches remote machines.

Execution policy is the last Windows-only setting. Get-ExecutionPolicy -List on Server 2025 showed RemoteSigned at LocalMachine for both shells. On Linux the policy is fixed at Unrestricted and Set-ExecutionPolicy RemoteSigned fails with Operation is not supported on this platform.

How does PowerShell remoting work over SSH and WinRM?

PowerShell 7 remotes over SSH on every platform with -HostName, and over WinRM on Windows with -ComputerName. SSH is the one that crosses operating systems, and it is what we used to run commands on Windows Server 2025 from the Ubuntu box. You need three things on each target:

  • An SSH server that is running and reachable on port 22.
  • A Subsystem powershell line in the sshd config that starts pwsh in server mode.
  • Key-based login for the remote user, so scripts never wait on a password prompt.

Ubuntu reads drop-in files from /etc/ssh/sshd_config.d/, so the subsystem line can live in its own file and survive package updates to the main config:

echo 'Subsystem powershell /usr/bin/pwsh -sshs -NoLogo' | sudo tee /etc/ssh/sshd_config.d/50-powershell.conf
sudo sshd -t
sudo systemctl restart ssh

sshd -t returned nothing, which means the syntax is valid, and sudo sshd -T | grep -i subsystem then listed both the powershell and sftp subsystems. Without that line the client connects and then gives up with an error that never mentions the config (it is listed in the troubleshooting section below).

Windows Server 2025 already had the OpenSSH server capability installed, sshd running and the inbound firewall rule enabled, so only the config needed changes. Open the server config:

notepad C:\ProgramData\ssh\sshd_config

Add the subsystem line next to the sftp one. The Microsoft docs use the 8.3 short path because sshd cannot handle the space in Program Files, and we added -NoLogo to match the Linux side. Our image also shipped with the administrators block commented out, so we enabled it, which makes admin accounts read their keys from administrators_authorized_keys:

Subsystem powershell c:/progra~1/powershell/7/pwsh.exe -sshs -NoLogo

Match Group administrators
       AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys

The Linux side needs a key pair and the Windows host key before the first connection. We created the key with ssh-keygen -t ed25519 as the Linux user, added the server’s host key with ssh-keyscan 10.0.1.51 >> ~/.ssh/known_hosts, and created a local Windows account in the Administrators group to log in as. Paste the Linux user’s public key into C:\ProgramData\ssh\administrators_authorized_keys, lock the file down to Administrators and SYSTEM, and restart sshd. The Microsoft docs require that ACL, and key login fails when the file is readable by other accounts:

icacls C:\ProgramData\ssh\administrators_authorized_keys /inheritance:r /grant "Administrators:F" /grant "SYSTEM:F"
Restart-Service sshd

From the Ubuntu box, one Invoke-Command now reaches both operating systems. The screenshot shows the first run against Windows and a fan-out to both hosts:

PowerShell 7 SSH remoting from Ubuntu 26.04 to Windows Server 2025 with Invoke-Command -HostName

The PSComputerName column tells you which host each result came from, which matters once a block runs on twenty servers. The post-quantum warning comes from the Ubuntu 26.04 OpenSSH client because the Windows server ships OpenSSH 9.5, which lacks the newer key exchange. It is a warning only, the session works, and the lasting fix is a newer OpenSSH on the Windows side. Key generation and known_hosts handling on the Linux end are covered in our SSH commands cheat sheet.

WinRM is still the default between Windows machines, and it has one behaviour that catches people out. From pwsh, Invoke-Command -ComputerName without a configuration name connects to the Microsoft.PowerShell endpoint, which is Windows PowerShell 5.1. We ran the same version check against both endpoints:

Invoke-Command -ComputerName localhost -Credential $cred -ScriptBlock { $PSVersionTable.PSVersion.ToString() }
Invoke-Command -ComputerName localhost -Credential $cred -ConfigurationName PowerShell.7 -ScriptBlock { $PSVersionTable.PSVersion.ToString() }

The first came back as 5.1 and the second as 7, even though both ran from the same pwsh window:

5.1.26100.7462
7.6.9

So a script that uses ?? or -Parallel inside the remote block fails on the default endpoint even though you started it from pwsh. Pass -ConfigurationName PowerShell.7, or set $PSSessionConfigurationName = 'PowerShell.7' in your profile. The PowerShell.7 endpoint exists because the MSI ran with ENABLE_PSREMOTING=1; on a machine installed without it, run Enable-PSRemoting -Force from pwsh as an administrator. That command only touches PowerShell 6+ endpoints and says so:

PowerShell remoting has been enabled only for PowerShell 6+ configurations and does not affect Windows PowerShell remoting configurations. Run this cmdlet in Windows PowerShell to affect all PowerShell remoting configurations.
WinRM is already set up to receive requests on this computer.
WinRM is already set up for remote management on this computer.

Linux cannot use -ComputerName at all: Invoke-Command -ComputerName localhost on Ubuntu failed with the WSMan error listed further down. For mixed fleets, standardize on SSH and keep WinRM for Windows-to-Windows work. If you manage .NET code from PowerShell as well, our guide on working with .NET as a PowerShell developer picks up from here.

How do you install modules with PSResourceGet?

Use Install-PSResource with -TrustRepository in scripts, or trust the PowerShell Gallery once with Set-PSResourceRepository. PowerShell 7 ships both module managers: Microsoft.PowerShell.PSResourceGet (the *-PSResource cmdlets) and the older PowerShellGet with Install-Module. They install to the same folders, so a module installed with one shows up in Get-InstalledPSResource and Get-InstalledModule alike.

The gallery starts out untrusted, and that is where unattended installs go wrong. Searching is fine, but an install without -TrustRepository shows a confirmation prompt, and in a non-interactive session it then fails with an error that says nothing about trust:

Untrusted repository
You are installing the modules from an untrusted repository. If you trust this
repository, change its Trusted value by running the Set-PSResourceRepository
cmdlet. Are you sure you want to install the PSResource from 'PSGallery'?
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help
(default is "N"):Object reference not set to an instance of an object.

Adding -Confirm:$false did not help; the same prompt and the same failure came back. Only -TrustRepository installed the module:

Install-PSResource -Name PSScriptAnalyzer -Scope CurrentUser -TrustRepository
Get-InstalledPSResource

The module then lists with its version and source. For build agents and provisioning scripts, settle on one of these so no install ever stops at a prompt:

  • Every Install-PSResource call in the script carries -TrustRepository.
  • The image runs Set-PSResourceRepository -Name PSGallery -Trusted once, and Get-PSResourceRepository shows Trusted as True.
  • Internal builds point at your own repository, registered with Register-PSResourceRepository and marked trusted.

We trusted and then un-trusted PSGallery on the Ubuntu box, and Get-PSResourceRepository flipped between True and False as expected. This is what the installed module looked like:

Name             Version Prerelease Repository Description
----             ------- ---------- ---------- -----------
PSScriptAnalyzer 1.25.0             PSGallery  PSScriptAnalyzer provides script analysis and checks for potential code defects in the scrip…

PSScriptAnalyzer is worth having for exactly the alias problem covered earlier. Pointed at function f { gci }, it flagged PSAvoidUsingCmdletAliases with the message 'gci' is an alias of 'Get-ChildItem'. The legacy Install-Module -Name Pester -Scope CurrentUser -Force installed Pester with no prompt at all on the same untrusted gallery. That is convenient, but -Force also overrides other install warnings, which is why we prefer the explicit -TrustRepository switch.

Modules land in different folders per platform, which matters when you copy one by hand or bake it into an image:

ScopeUbuntu 26.04Windows Server 2025
Current user~/.local/share/powershell/ModulesDocuments\PowerShell\Modules
All users/usr/local/share/powershell/ModulesC:\Program Files\PowerShell\Modules
Shipped with pwsh/opt/microsoft/powershell/7/ModulesC:\Program Files\PowerShell\7\Modules
Windows PowerShell 5.1 pathsNoneAlso on the path, after the pwsh folders

Windows pwsh also searches the 5.1 module folders after its own, so modules written for 5.1 are found too. The ones that need the full .NET Framework load through the compatibility session, the way ServerManager did in the Windows section.

Where does the PowerShell profile live?

$PROFILE holds the path of your current-user, current-host profile, and it has three siblings for the other scopes. On Ubuntu the file does not exist until you create it:

$PROFILE | Select-Object *
New-Item -ItemType File -Path $PROFILE -Force

Linux follows the XDG layout, so the user profiles sit under ~/.config/powershell and the all-users ones next to the binary:

AllUsersAllHosts       : /opt/microsoft/powershell/7/profile.ps1
AllUsersCurrentHost    : /opt/microsoft/powershell/7/Microsoft.PowerShell_profile.ps1
CurrentUserAllHosts    : /home/harrison/.config/powershell/profile.ps1
CurrentUserCurrentHost : /home/harrison/.config/powershell/Microsoft.PowerShell_profile.ps1

On Windows the user profile lives in Documents\PowerShell, a different folder from 5.1’s Documents\WindowsPowerShell, so your old profile does not follow you to pwsh. Copy what you still need. An empty profile cost us nothing measurable at startup (418 to 440 ms against 391 to 428 ms without it), but every module you import there adds to each new shell.

Keep the profile lean, because it runs in every new interactive shell and in every scheduled pwsh call started without -NoProfile, so a slow import is paid many times a day. Before you commit a profile to your dotfiles, check it against this list:

  • pwsh -c exit starts within a few hundred milliseconds of pwsh -NoProfile -c exit.
  • The profile imports only modules you use in most sessions.
  • Windows-only lines sit inside if ($IsWindows) { } when the same profile syncs to Linux or macOS.

PSReadLine handles the command line itself. By default it ran in Emacs edit mode with HistoryAndPlugin predictions shown inline, Ctrl+r bound to reverse history search, and history saved to ~/.local/share/powershell/PSReadLine/ConsoleHost_history.txt. That history file keeps every command you type across sessions, including any secret you pasted on the command line, so clear it after a mistake. For a Git-aware prompt on Windows, posh-git adds branch and status to it.

How do you fix common PowerShell 7 errors?

These are the errors we actually hit while building the lab, with the cause and the fix for each. Most of them come from a command that works on one platform being run on the other, so the message rarely points at the real problem.

Error: “subsystem request failed on channel 0”

The target’s sshd has no Subsystem powershell line. SSH itself connected, which is why the full message also says The SSH transport process has abruptly terminated causing this remote session to break. Add the subsystem line from the remoting section, run sshd -t and restart sshd.

Error: “This parameter set requires WSMan, and no supported WSMan client library was found”

You used -ComputerName on Linux or macOS, which means WinRM, and pwsh on those platforms has no WSMan client. Switch to -HostName and -UserName so the session runs over SSH.

Error: “Connecting to remote server localhost failed … Access is denied”

We got this on Windows when running Invoke-Command -ComputerName localhost as the SYSTEM account, which has no network credential for WinRM. A normal admin session or an explicit -Credential for a local administrator fixed it on all three endpoints.

Error: “Object reference not set to an instance of an object” from Install-PSResource

The trust prompt for the untrusted PSGallery had no one to answer it. Add -TrustRepository to the install, or run Set-PSResourceRepository -Name PSGallery -Trusted once on build machines.

Error: “The term ‘Get-Service’ is not recognized”

The cmdlet only exists on Windows. On Linux, call systemctl directly from pwsh, for example systemctl is-active ssh, and wrap Windows-only blocks in if ($IsWindows) { ... } in shared scripts.

Error: “Operation is not supported on this platform” from Set-ExecutionPolicy

Execution policy is a Windows feature. Linux and macOS always run as Unrestricted, so remove the call from cross-platform setup scripts or guard it with $IsWindows.

Error: “E: Unable to locate package powershell” on Ubuntu 26.04

Microsoft’s 26.04 apt feed only had powershell-preview when we tested. Install the .deb from the GitHub release as shown in the install section, and check apt-cache policy powershell again later to see whether the stable package has arrived.

Checklist: set up PowerShell 7 on a new machine

Run through this list on each new workstation or server. Every item is something you can verify from the shell in a few seconds:

  1. pwsh --version prints the release you expect, and $PSVersionTable.PSEdition reads Core.
  2. Update-Help -UICulture en-US has run for your account (or once with -Scope AllUsers), so Get-Help shows full examples.
  3. Test-Path $PROFILE returns True, and the profile only loads modules you use daily.
  4. Get-PSResourceRepository shows PSGallery trusted, or your scripts pass -TrustRepository.
  5. sshd -T lists the powershell subsystem on every host you plan to reach with -HostName.
  6. On Windows, Get-PSSessionConfiguration lists PowerShell.7, and your WinRM calls pass -ConfigurationName PowerShell.7.
  7. Shared scripts use full cmdlet names, and Invoke-ScriptAnalyzer reports no PSAvoidUsingCmdletAliases warnings.

If you’re still choosing a daily shell for Linux or macOS, our comparison of Bash, Zsh, Fish, PowerShell and Nushell shows where pwsh fits next to them. Windows admins who want more depth can pick from our Windows Server administration books.

Keep reading

Claude Code Cheat Sheet: Commands, Shortcuts and Flags (PDF) AI Claude Code Cheat Sheet: Commands, Shortcuts and Flags (PDF) OpenAI Codex CLI Cheat Sheet: Shortcuts, Commands, Flags (PDF) AI OpenAI Codex CLI Cheat Sheet: Shortcuts, Commands, Flags (PDF) Create a Bootable Windows 11 USB and Install Windows 11 Windows Create a Bootable Windows 11 USB and Install Windows 11 APT Cheat Sheet: apt and apt-get Commands for Ubuntu/Debian Cheat Sheets APT Cheat Sheet: apt and apt-get Commands for Ubuntu/Debian macOS Keyboard Shortcuts Cheat Sheet (Golden Gate and Tahoe) Cheat Sheets macOS Keyboard Shortcuts Cheat Sheet (Golden Gate and Tahoe) Install Active Directory Domain Services in Windows Server 2022 Windows Install Active Directory Domain Services in Windows Server 2022

Leave a Comment

Press ESC to close