Skip to content

Instantly share code, notes, and snippets.

@Digiover
Last active August 25, 2026 12:09
Show Gist options
  • Select an option

  • Save Digiover/e6a49daf927e68baf0ecb03c3f144b2d to your computer and use it in GitHub Desktop.

Select an option

Save Digiover/e6a49daf927e68baf0ecb03c3f144b2d to your computer and use it in GitHub Desktop.
Install or update SQL Server Management Studio automatically using this fire-and-forget method
<#
.SYNOPSIS
Installs or updates SQL Server Management Studio (SSMS) 21 or later, unattended.
.DESCRIPTION
Starting with SSMS 21, SQL Server Management Studio ships as part of the Visual Studio
installer family. There is no standalone MSI any more. The download is a ~5 MB stub
installer (bootstrapper) named vs_SSMS.exe, which hands the actual work to the Visual
Studio Installer.
That breaks every script written for SSMS 20 and earlier:
- The old setup accepted /Install /Quiet. The bootstrapper wants --quiet --wait --norestart.
- Without --wait the bootstrapper returns immediately while the install continues in the
background, so a zero exit code means nothing.
- SSMS 21 and later register in the uninstall registry as "SQL Server Management Studio 21",
without the "Microsoft " prefix that SSMS 17 to 20 used. A filter on
"Microsoft SQL Server Management Studio*" silently finds nothing.
- VersionMajor is empty for those entries, and the install location moved from
"C:\Program Files (x86)\Microsoft SQL Server Management Studio <n>" to
"C:\Program Files\Microsoft SQL Server Management Studio <n>\Release".
- UninstallString points at the Visual Studio Installer (setup.exe) with its own
"uninstall --installPath" arguments, which are not /uninstall /quiet.
This script handles both worlds. It detects what is installed, updates an existing
SSMS 21+ installation in place, installs it side by side when the requested major
version is missing, and can optionally remove a legacy SSMS 20 or earlier install.
Uninstall-then-install, the approach that was needed in the MSI era, is no longer
required: SSMS 21+ supports side-by-side installs and has an "update" verb.
.PARAMETER Major
Major SSMS version to install or update. Defaults to 22.
.PARAMETER Channel
Release channel to pull the bootstrapper from. Defaults to release.
.PARAMETER BootstrapperUri
Overrides the download URI completely. Use this to pin a specific servicing release from
the SSMS release history page.
.PARAMETER InstallPath
Optional install directory, passed as --installPath. Can only be set on first install.
.PARAMETER Add
Optional workload or component IDs to add, for example
Microsoft.SqlServer.Workload.SSMS.CodeTools. Repeatable.
.PARAMETER IncludeRecommended
Adds --includeRecommended, pulling in the recommended components of the selected workloads.
.PARAMETER RemoveLegacy
Also uninstall any SSMS 20 or earlier (MSI era) installation that is found.
.PARAMETER Force
Passes --force, which closes a running SSMS instead of failing with exit code 1003 or 8006.
This can lose unsaved work.
.PARAMETER KeepBootstrapper
Keeps the downloaded vs_SSMS.exe instead of deleting it afterwards.
.EXAMPLE
.\Update-SqlServerManagementStudio.ps1
Installs or updates SSMS 22 silently.
.EXAMPLE
.\Update-SqlServerManagementStudio.ps1 -Major 22 -RemoveLegacy -Force
Removes an old SSMS 20 or earlier, then installs SSMS 22, closing SSMS if it is running.
.EXAMPLE
.\Update-SqlServerManagementStudio.ps1 -Add Microsoft.SqlServer.Workload.SSMS.CodeTools -IncludeRecommended
Installs SSMS 22 including the Code Tools workload and its recommended components.
.NOTES
Update of: https://www.saotn.org/windows-server/update-sql-server-management-studio-ssms-automatically/
Requires an elevated session. Visual Studio Installer operations need administrator rights.
Twitter / X: @Jan_Reilink
#>
#Requires -Version 5.1
#Requires -RunAsAdministrator
[CmdletBinding(SupportsShouldProcess)]
param(
[ValidateRange(21, 99)]
[int]$Major = 22,
[ValidateSet('release', 'preview')]
[string]$Channel = 'release',
[uri]$BootstrapperUri,
[string]$InstallPath,
[string[]]$Add,
[switch]$IncludeRecommended,
[switch]$RemoveLegacy,
[switch]$Force,
[switch]$KeepBootstrapper
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# Documented Visual Studio Installer error codes. See the SSMS command-line parameters page.
$script:ExitCodes = @{
0 = 'Operation completed successfully'
740 = 'Elevation required'
1001 = 'Visual Studio installer process is running'
1003 = 'SSMS is in use'
1602 = 'Operation was canceled'
1618 = 'Another installation running'
1641 = 'Operation completed successfully, and reboot was initiated'
3010 = 'Operation completed successfully, but install requires reboot before it can be used'
5003 = 'Bootstrapper failed to download installer'
5004 = 'Operation was canceled'
5005 = 'Bootstrapper command-line parse error'
5007 = "Operation was blocked - the computer doesn't meet the requirements"
8001 = 'Arm machine check failure'
8002 = 'Background download precheck failure'
8003 = 'Out of support selectable failure'
8004 = 'Target directory failure'
8005 = 'Verifying source payloads failure'
8006 = 'SSMS processes running'
8010 = 'Operating system not supported'
-1073720687 = 'Connectivity failure'
-1073741510 = 'Microsoft Visual Studio Installer was terminated'
}
# 0 = done, 1641 = done and rebooting, 3010 = done but needs a reboot first.
$script:SuccessCodes = @(0, 1641, 3010)
function Get-RegistryValue {
<#
Reads one property off a registry object, returning $null when it is absent.
Get-ItemProperty only materialises properties for values that actually exist, so plenty of
uninstall keys have no DisplayName, InstallLocation or UninstallString at all. Under
Set-StrictMode -Version Latest a direct $_.DisplayName on such a key is a terminating error,
not an empty string.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)][AllowNull()]$InputObject,
[Parameter(Mandatory)][string]$Name
)
if ($null -eq $InputObject) { return $null }
$property = $InputObject.PSObject.Properties[$Name]
if ($null -eq $property) { return $null }
return $property.Value
}
function Get-SsmsInstallation {
<#
Returns every SQL Server Management Studio installation found in the uninstall registry.
InstallerType tells the two generations apart:
VisualStudio = SSMS 21 and later, driven by the Visual Studio Installer
Legacy = SSMS 20 and earlier, the standalone SSMS-Setup-ENU.exe installer
Note the DisplayName filter: it deliberately matches with and without the "Microsoft "
prefix, because SSMS 21+ dropped it. It does not use a wildcard on "SSMS", which would
also catch unrelated entries such as vs_ssmsprotocolselectormsi.
#>
[CmdletBinding()]
param()
$uninstallKeys = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
$entries = @(
Get-ItemProperty -Path $uninstallKeys -ErrorAction SilentlyContinue |
Where-Object { (Get-RegistryValue -InputObject $_ -Name 'DisplayName') -like '*SQL Server Management Studio*' }
)
foreach ($entry in $entries) {
$displayName = Get-RegistryValue -InputObject $entry -Name 'DisplayName'
$displayVersion = Get-RegistryValue -InputObject $entry -Name 'DisplayVersion'
$installLocation = Get-RegistryValue -InputObject $entry -Name 'InstallLocation'
$uninstallString = Get-RegistryValue -InputObject $entry -Name 'UninstallString'
$isVisualStudio = $uninstallString -match 'Microsoft Visual Studio\\Installer\\setup\.exe'
# VersionMajor is empty for the Visual Studio Installer entries, so derive the major
# version from the display name first and fall back to the install location.
$major = $null
if ($displayName -match 'Management Studio\D*(\d+)') {
$major = [int]$Matches[1]
}
elseif ($installLocation -and $installLocation -match 'Management Studio\D*(\d+)') {
$major = [int]$Matches[1]
}
[pscustomobject]@{
DisplayName = $displayName
Major = $major
Version = $displayVersion
InstallLocation = $installLocation
UninstallString = $uninstallString
InstallerType = if ($isVisualStudio) { 'VisualStudio' } else { 'Legacy' }
}
}
}
function Get-SsmsBootstrapper {
<#
Downloads the vs_SSMS.exe stub installer and returns the path to it.
aka.ms/ssmsfullsetup redirects to the current major version, which makes it a moving
target for a script that has to decide between install and update. The per-version URI
is deterministic, so that is the default.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)][uri]$Uri,
[Parameter(Mandatory)][string]$Destination
)
Write-Host "Downloading stub installer from $Uri"
$previousProgress = $ProgressPreference
$ProgressPreference = 'SilentlyContinue'
try {
Invoke-WebRequest -Uri $Uri -OutFile $Destination -UseBasicParsing
}
finally {
$ProgressPreference = $previousProgress
}
$item = Get-Item -LiteralPath $Destination
# The product version of the bootstrapper tells you which channel and version it installs.
$productVersion = $item.VersionInfo.ProductVersion
Write-Host ("Downloaded {0} ({1:N1} MB, product version {2})" -f $item.Name, ($item.Length / 1MB), $productVersion)
return $item.FullName
}
function Invoke-SsmsSetup {
<#
Runs the bootstrapper or the Visual Studio Installer and reports the documented exit code.
Returns $true when the operation succeeded.
--wait matters: it is the only reason the exit code means anything. Without it the
bootstrapper returns as soon as it has handed off to the Visual Studio Installer.
Note that --wait is a bootstrapper-only parameter; setup.exe does not support it, which
is why the caller relies on Start-Process -Wait there.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$FilePath,
[Parameter(Mandatory)][string[]]$Arguments
)
Write-Host "Running: `"$FilePath`" $($Arguments -join ' ')"
$process = Start-Process -FilePath $FilePath -ArgumentList $Arguments -Wait -PassThru
$code = $process.ExitCode
$meaning = if ($script:ExitCodes.ContainsKey($code)) { $script:ExitCodes[$code] } else { 'Failure condition occurred - check the logs in %TEMP% (ssms_bootstrapper*, ssms_client*, ssms_setup*)' }
if ($script:SuccessCodes -contains $code) {
Write-Host "Exit code ${code}: $meaning"
return $true
}
Write-Warning "Exit code ${code}: $meaning"
return $false
}
function Remove-LegacySsms {
<#
Uninstalls an SSMS 20 or earlier installation. Those still use the standalone installer
that accepts single-dash switches. Returns $true when everything was removed.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)][pscustomobject[]]$Installation
)
$allRemoved = $true
foreach ($ssms in $Installation) {
Write-Host "Removing legacy installation: $($ssms.DisplayName)"
if ([string]::IsNullOrWhiteSpace($ssms.UninstallString)) {
Write-Warning "No UninstallString for $($ssms.DisplayName), skipping."
$allRemoved = $false
continue
}
# Workaround kept from the original script: the uninstall of some older SSMS builds
# trips over this leftover binary. VersionMajor is unreliable, so glob the folder.
$deploymentExe = @(
Get-ChildItem -Path 'C:\Program Files (x86)\Microsoft SQL Server Management Studio *\Common7\IDE\Microsoft.AnalysisServices.Deployment.exe' -ErrorAction SilentlyContinue
)
foreach ($exe in $deploymentExe) {
Write-Host "Removing $($exe.FullName)"
Remove-Item -LiteralPath $exe.FullName -Force -ErrorAction SilentlyContinue
}
$executable = $ssms.UninstallString.Split('"')[1]
if ([string]::IsNullOrWhiteSpace($executable)) {
$executable = $ssms.UninstallString
}
if (-not (Invoke-SsmsSetup -FilePath $executable -Arguments @('/uninstall', '/quiet', '/norestart'))) {
$allRemoved = $false
}
}
return $allRemoved
}
# ---------------------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------------------
if (-not $BootstrapperUri) {
$BootstrapperUri = "https://aka.ms/ssms/$Major/$Channel/vs_SSMS.exe"
}
# @() around the call: a single match would otherwise unroll into a bare object and the
# .Count and array operations below would behave differently.
$installations = @(Get-SsmsInstallation)
if ($installations.Count -eq 0) {
Write-Host 'No SQL Server Management Studio installation found.'
}
else {
Write-Host 'Found the following SQL Server Management Studio installations:'
$installations |
Select-Object DisplayName, Version, InstallerType, InstallLocation |
Format-Table -AutoSize |
Out-String |
Write-Host
}
$legacy = @($installations | Where-Object { $_.InstallerType -eq 'Legacy' })
$target = @($installations | Where-Object { $_.InstallerType -eq 'VisualStudio' -and $_.Major -eq $Major })
if ($RemoveLegacy -and $legacy.Count -gt 0) {
if ($PSCmdlet.ShouldProcess(($legacy.DisplayName -join ', '), 'Uninstall legacy SSMS')) {
if (-not (Remove-LegacySsms -Installation $legacy)) {
Write-Warning 'Removing the legacy installation did not fully succeed. Continuing anyway.'
}
}
}
elseif ($legacy.Count -gt 0) {
Write-Host "Legacy installation present ($($legacy.DisplayName -join ', ')). It can stay side by side; pass -RemoveLegacy to uninstall it."
}
$bootstrapperPath = Join-Path -Path $env:TEMP -ChildPath 'vs_SSMS.exe'
try {
if (-not $PSCmdlet.ShouldProcess("SSMS $Major ($Channel)", $(if ($target.Count -gt 0) { 'Update' } else { 'Install' }))) {
return
}
$bootstrapperPath = Get-SsmsBootstrapper -Uri $BootstrapperUri -Destination $bootstrapperPath
# An existing install of this major version gets the update verb. A different major
# version (SSMS 21 next to 22) is a separate product and lands side by side.
$arguments = [System.Collections.Generic.List[string]]::new()
if ($target.Count -gt 0) {
Write-Host "SSMS $Major is already installed ($($target[0].Version)). Updating in place."
$arguments.Add('update')
# For update the install path is required to identify the instance to act upon.
if ($target[0].InstallLocation) {
$arguments.Add('--installPath')
$arguments.Add($target[0].InstallLocation)
}
}
else {
Write-Host "SSMS $Major is not installed. Performing a fresh install."
if ($InstallPath) {
$arguments.Add('--installPath')
$arguments.Add($InstallPath)
}
foreach ($id in $Add) {
$arguments.Add('--add')
$arguments.Add($id)
}
if ($IncludeRecommended) {
$arguments.Add('--includeRecommended')
}
}
$arguments.Add('--quiet')
$arguments.Add('--wait')
$arguments.Add('--norestart')
if ($Force) {
$arguments.Add('--force')
}
if (Invoke-SsmsSetup -FilePath $bootstrapperPath -Arguments $arguments.ToArray()) {
$result = @(Get-SsmsInstallation | Where-Object { $_.InstallerType -eq 'VisualStudio' -and $_.Major -eq $Major })
if ($result.Count -gt 0) {
Write-Host "SQL Server Management Studio $Major is now at version $($result[0].Version)."
}
else {
Write-Warning "Setup reported success but SSMS $Major was not found in the registry afterwards."
}
}
else {
Write-Warning 'SQL Server Management Studio setup did not complete successfully.'
}
}
finally {
if (-not $KeepBootstrapper -and (Test-Path -LiteralPath $bootstrapperPath)) {
Remove-Item -LiteralPath $bootstrapperPath -Force -ErrorAction SilentlyContinue
}
}
@Digiover

Copy link
Copy Markdown
Author

The fire-and-forget method to update SQL Server Management Studio (SSMS) automatically by uninstalling any installed version and downloading & installing the newest available version from Microsoft.

Blogpost: https://www.saotn.org/windows-server/update-sql-server-management-studio-ssms-automatically/

@Digiover

Copy link
Copy Markdown
Author

Since SQL Server Management Studio 21, the URL https://aka.ms/ssmsfullsetup redirects to a stub installer file vs_SSMS.exe, breaking the installation part of the previous version of this script. Here is an updated version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment