PowerShell 7.7 Is Out — Upgrade, or Stay on 7.6 LTS?
PowerShell 7.7.0 and the 7.6.10 LTS update both came out on November 10. What you get on 7.7, what you miss by staying on 7.6, and how to write scripts that behave the same on both.

On November 10 the PowerShell team released 7.7.0, built on .NET 11, together with 7.6.10 for the 7.6 LTS line. The same day, 7.4 and 7.5 went out of support (see PowerShell 7.4 and 7.5 Retire on November 10).
My own machines always run the latest version, so 7.7 is already installed there. Work is a different story: our Function apps and Automation runbooks run on the PowerShell 7.6 runtime, so anything I write for those has to work on 7.6. That makes the useful question less “should I upgrade?” and more “what am I missing on 7.6, do I actually need it, and is there a workaround that runs on both?”
That’s what this post is about. For what 7.6 itself brought, see What’s New in PowerShell 7.6.
Support: closer than you’d think
The usual reason to stay on an LTS release is a longer support window. This time that difference is mostly gone. PowerShell’s support follows the .NET version underneath it (support lifecycle), and since Microsoft stretched STS releases to 24 months, .NET 11 ends on the same day as .NET 10:
| Version | Built on | .NET end of support |
|---|---|---|
| PowerShell 7.6 (LTS) | .NET 10 (LTS) | November 14, 2028 |
| PowerShell 7.7 | .NET 11 (STS) | November 14, 2028 |
The PowerShell lifecycle page doesn’t list a date for 7.7 yet, so treat that second row as “if it follows .NET, like 7.5 did”.
The real difference is what “supported” means in practice. An LTS release only gets security and servicing fixes, so 7.6 stays put. A stable release like 7.7 is where new features and behaviour changes land. And only the latest patch of a release is supported, on both lines.
What 7.6.10 brings
If you’re staying on 7.6, update to 7.6.10. It’s a small release:
- A warning when you create or validate a file catalog with the legacy SHA-1 algorithm (
New-FileCatalog/Test-FileCatalog). It recommends-CatalogVersion 2, which uses SHA-256. Hashing and validation work exactly as before. (#28311) - A fix for a
Test-Jsonregression from 7.6.7: when a schema violation sat deeper than the top level (an array item, for example),Test-JsonreturnedFalsebut no longer told you what was wrong or where (#28218, fixed by #28273). If you validate JSON in a pipeline, this one is worth having. - Better detection of the MSIX (Store) install. (#28313)
- An updated .NET 10 SDK underneath.
You’re missing less than you think
Several features that started in 7.7 previews were backported to the 7.6 line during the autumn. If you’re on the latest 7.6, you already have these:
| Feature | In 7.6 since |
|---|---|
$PSStyle.AutoSizeDefaultFormatting — make default table/wide output behave like -AutoSize | 7.6.7 |
A broken per-user powershell.config.json gives a warning instead of stopping pwsh from starting | 7.6.7 |
| No more update notification in the startup banner (the feature was removed) | 7.6.8 |
| SHA-1 file catalog warning | 7.6.10 |
The tolerant config one was triggered by OneDrive: when the file sits in a OneDrive folder that isn’t available yet (for example right after logging in), PowerShell used to refuse to start (#27373).
The update notification removal is worth knowing if, like me, you rely on it to tell you a new version is out. It’s gone on both versions, so update through your package manager instead (winget upgrade Microsoft.PowerShell, brew upgrade, or the Store).
A 7.6 runtime in Azure Functions or Azure Automation isn’t necessarily on 7.6.10. Before relying on a backported feature there, check $PSVersionTable.PSVersion from inside the function or runbook.
What you miss on 7.6, and the workaround
These are the 7.7 additions I’d actually use, each with a version that works on both. I ran every snippet on 7.6.10 and 7.7.0 side by side.
New-TemporaryDirectory
7.7 adds New-TemporaryDirectory, the folder counterpart of New-TemporaryFile. It returns a DirectoryInfo for a new, uniquely named folder under the system temp path.
$dir = New-TemporaryDirectory
$dir.GetType().FullNameOn 7.7.0:
System.IO.DirectoryInfoOn 7.6.10 you get The term 'New-TemporaryDirectory' is not recognized as a name of a cmdlet, function, script file, or executable program. But .NET has had the same thing since .NET 7, and this works on both:
$dir = [System.IO.Directory]::CreateTempSubdirectory('blog-')
$dir.GetType().FullName
$dir.Name.StartsWith('blog-')Same output on 7.6.10 and 7.7.0:
System.IO.DirectoryInfo
TrueDo you need the cmdlet? Only in Constrained Language Mode, where calling .NET types directly isn’t allowed. That was the reason it was added. Everywhere else, the .NET method does the job and even lets you set a prefix.
Join-Path -Extension
Join-Path can now change (or add, or remove) the extension of the path it builds:
Join-Path -Path '/reports' -ChildPath 'export.csv' -Extension '.json'On 7.7.0:
/reports/export.jsonOn 7.6.10 I expected a “parameter not found” error. Instead I got this:
/reports/export.csv/-Extension/.jsonJoin-Path has a parameter, -AdditionalChildPath, that collects any leftover arguments as extra child paths. On 7.6, -Extension and .json end up there as two more folder names, so you get a wrong path and no error. If 7.7 code like this ends up on a 7.6 runtime, nothing tells you.
The way that works on both is [System.IO.Path]::ChangeExtension(), which is what the cmdlet uses under the hood:
[System.IO.Path]::ChangeExtension((Join-Path -Path '/reports' -ChildPath 'export.csv'), '.json')Same output on 7.6.10 and 7.7.0:
/reports/export.jsonDo you need it? No. Nicer to read, nothing more. But given the warning above, I’d stay away from it in any code that might run on 7.6.
Format-* -ExcludeProperty
All four Format-* cmdlets get -ExcludeProperty, so you can drop a noisy column without an extra Select-Object:
$server = [pscustomobject]@{ Name = 'web01'; Role = 'IIS'; Notes = 'Long text nobody needs in a table' }
$server | Format-Table -ExcludeProperty NotesOn 7.7.0:
Name Role
---- ----
web01 IISOn 7.6.10 this one does fail properly: A parameter cannot be found that matches parameter name 'ExcludeProperty'. The way that works on both gives the same table:
$server | Select-Object -Property * -ExcludeProperty Notes | Format-TableName Role
---- ----
web01 IISDo you need it? No. It’s for the console, and formatted output doesn’t belong in a Function app or runbook anyway.
$PSApplicationOutputEncoding
When PowerShell reads the output of an external program, it decodes the bytes with [Console]::OutputEncoding. If the program writes in a different encoding, you get mangled text. wsl.exe is the usual example, because it writes UTF-16 (two bytes per character). cmd /u does the same for its built-in commands, which makes it an easy test on any Windows machine.
I ran this on Windows with both versions. The console was on code page 850, the default for a Dutch Windows install, and my only WSL distro is docker-desktop:
$distros = wsl.exe --list --quiet
$distros -contains 'docker-desktop'Instead of one line with docker-desktop, $distros held three lines: the name with a null character after every letter (28 characters instead of 14), followed by two lines that held nothing but a null character. Same on 7.6.10 and 7.7.0.
I expected the -contains check to fail on that, but it returned True. PowerShell’s comparison operators are culture-aware and apparently skip the null characters. So the check you’d write first looks fine, while the data itself is broken: it has the wrong length, extra lines, and nulls in anything you write to a file or pass on.
The way that works on both is to change [Console]::OutputEncoding for the duration of the call, and change it back afterwards:
$previous = [Console]::OutputEncoding
try {
[Console]::OutputEncoding = [System.Text.Encoding]::Unicode
$distros = wsl.exe --list --quiet
}
finally {
[Console]::OutputEncoding = $previous
}That returned one clean docker-desktop (14 characters) on both versions. The downside is that [Console]::OutputEncoding is process-wide: while it’s changed, everything else running in the same process sees the change too, such as ForEach-Object -Parallel and other runspaces.
7.7 adds $PSApplicationOutputEncoding (#21219). It only affects how PowerShell reads output from external programs, and it’s an ordinary variable, so it only applies in the scope where you set it:
function Get-WslDistro {
$PSApplicationOutputEncoding = [System.Text.Encoding]::Unicode
wsl.exe --list --quiet
}
Get-WslDistroOn 7.7.0 that returned the same clean docker-desktop. Calling wsl.exe directly again after the function gave the mangled output again, and [Console]::OutputEncoding was still on code page 850. The setting stayed inside the function.
WSL has its own switch: set the environment variable WSL_UTF8=1 and wsl.exe writes UTF-8 instead. With WSL 2.7.14 that also returned a clean docker-desktop on both versions, without touching PowerShell’s encoding at all.
Do you need it? Only if you call programs that don’t write in the encoding PowerShell expects. On 7.6, the try/finally or (for WSL) WSL_UTF8 does the job. The variable is mostly nicer in parallel code.
The rest
Smaller additions without a real workaround, and nothing I’d miss on a server: tab completion for $PSBoundParameters.Keys, a SubjectAlternativeName property on Get-AuthenticodeSignature output, WildcardPattern.ToRegex(), and a DSC v3 resource for your PowerShell profile (experimental). The What’s New in PowerShell 7.7 page has the full list.
Same script, different result
This is the part that matters if you write code that runs on both versions. Apart from Join-Path -Extension above, a few things behave differently on 7.7, mostly without an error to tell you.
New-Guid now makes version 7 GUIDs
New-Guid now creates a version 7 UUID instead of a random version 4 (#27033). Version 7 GUIDs start with a timestamp, so they sort by creation time. That’s nice for database keys. But it also means anyone who sees the GUID can read roughly when it was created, and the first part is predictable.
(New-Guid).Version
[guid]::CreateVersion7().Version
[guid]::NewGuid().Version| Line | 7.6.10 | 7.7.0 |
|---|---|---|
(New-Guid).Version | 4 | 7 |
[guid]::CreateVersion7().Version | 7 | 7 |
[guid]::NewGuid().Version | 4 | 4 |
Works on both: say which one you want. [guid]::NewGuid() is always random (v4), and [guid]::CreateVersion7() is always v7. Both are .NET methods, so they behave the same on 7.6 and 7.7.
Switches set to $false now mean $false
A long list of cmdlets used to check whether a switch was passed, not what its value was. So -Shuffle:$false behaved exactly like -Shuffle. 7.7 fixes this for Get-Random, Split-Path, Where-Object, Test-Connection, New-PSSession, the CSV cmdlets and a few more (full list).
You rarely type :$false yourself. Where you do hit it is a wrapper function that passes its own switch on:
function Get-Sample {
param([int[]]$Item, [switch]$All)
$Item | Get-Random -Shuffle:$All
}
(Get-Sample -Item (1..5)).CountCalled without -All, this should return one random item. 7.7.0 returns 1. 7.6.10 returns 5: it shuffled the whole list anyway.
Same with Split-Path:
Split-Path -Path '/reports/2026/export.csv' -Leaf:$false7.7.0 returns the parent folder, /reports/2026. 7.6.10 returns export.csv, the leaf you just said you didn’t want.
Works on both: don’t pass a switch as :$false to these cmdlets. Only add it when it’s on, for example with a splat:
function Get-Sample {
param([int[]]$Item, [switch]$All)
$randomParams = @{}
if ($All) { $randomParams['Shuffle'] = $true }
$Item | Get-Random @randomParams
}
(Get-Sample -Item (1..5)).Count
(Get-Sample -Item (1..5) -All).CountSame output on 7.6.10 and 7.7.0:
1
5Export-Csv -NoTypeInformation is obsolete
-NoTypeInformation has been the default behaviour since PowerShell 6, but it used to be something you added to every Export-Csv out of habit. I stopped using it a while ago, but if you haven’t, search your scripts and runbooks for it before they run on 7.7. In 7.7 it’s marked obsolete: it does nothing, but you get a warning every time (#26719).
[pscustomobject]@{ Name = 'web01'; Role = 'IIS' } | ConvertTo-Csv -NoTypeInformationOn 7.6.10:
"Name","Role"
"web01","IIS"On 7.7.0:
WARNING: Parameter 'NoTypeInformation' is obsolete. This parameter is obsolete and has no effect. The default behavior is to not include type information. Use -IncludeTypeInformation to include type information.
"Name","Role"
"web01","IIS"Works on both: remove -NoTypeInformation. The output is the same on 6.0 and later, without the warning.
Export-Csv -Append -NoHeader is now an error
That combination used to create a CSV without a header row, which then broke the next append (#26472):
$csv = Join-Path ([System.IO.Path]::GetTempPath()) 'append-test.csv'
[pscustomobject]@{ Name = 'web01' } | Export-Csv -Path $csv -Append -NoHeader7.6.10 says nothing and writes the file without a header. 7.7.0 stops with You must specify either the -Append or -NoHeader parameters, but not both.
Smaller breaking changes
Test-Jsononly resolves external$refschema references when you add-FetchExternalReference(#28205).- Empty or null arguments to parameters with not-null-or-empty validation now throw
System.ArgumentExceptioninstead ofPSArgumentNullException. This only matters if you catch that specific exception type (#26668).
No more MSI
7.7.0 ships no MSI installer for Windows: look at the release assets and you’ll find the MSIX bundle, ZIPs, and winget/Store. 7.6.10 still has MSIs.
From a packaging point of view I still prefer MSI over MSIX. I don’t do packaging professionally anymore, so that’s more of a feeling than a strong argument. If you deploy PowerShell to servers or through Intune with an MSI today, that’s a reason to stay on 7.6 for now, and to test how MSIX fits your deployment before the next LTS.
So, upgrade or not?
| You are… | My advice |
|---|---|
| Running scripts on your own machine | Upgrade. My machines are on 7.7 already. |
| Writing code for Azure Functions or Automation | Stay on 7.6. 7.7 isn’t available there, and since the portal normally only gets the LTS versions, I don’t expect it to be. |
| Writing a module others install | Target 7.6, and use the workarounds above instead of the 7.7-only features. |
| Deploying PowerShell to servers with an MSI | Stay on 7.6 until you’ve sorted out MSIX. |
All my 7.6 code runs in pipelines, Automation runbooks and Function apps, and it goes through a test environment before production. If I use something that doesn’t exist on 7.6, that shows up quickly there. The exceptions are the ones from this post that don’t throw an error: Join-Path -Extension, switches passed as :$false, and New-Guid. Those only show up if something actually checks the result, so they’re the ones to keep in mind while writing code on 7.7.
Happy scripting 😊