Update SQL Server Management Studio (SSMS) automatically

Date posted: 2025-07-07
Last updated: 2026-08-29

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.

The fire-and-forget method to update SQL Server Management Studio (SSMS) automatically. The script below starts with uninstalling any installed SSMS version. Once that is succeeded, it downloads & installs the latest available version from Microsoft.

This script is ideal for in your silent, automated (unattended) deployments of SQL Servers or management servers needing this tool.

Update: this is an updated version for SQL Server Management Studio 21, 22 and up.

Updated script for SSMS v21, 22 and up

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 above script. Here is an updated version.

Next to this script you can also use command-line parameters to install SQL Server Management Studio.

SSMS stub installer (vs_SSMS.exe) script

If you happen to have downloaded the SSMS stub installer file, you can also use this one to install SQL Server Management Studio unattended. Script:

<#
.SYNOPSIS
  Working reference for the SSMS stub installer (vs_SSMS.exe): every supported workflow,
  as a command you can print or run.

.DESCRIPTION
  Since SSMS 21 there is no standalone MSI. What you download is a stub installer, or
  bootstrapper, of roughly 5 MB called vs_SSMS.exe. It installs the Visual Studio Installer
  and hands the real work to it. That means there are two executables to drive, and they do
  not accept the same things:

    Bootstrapper   vs_SSMS.exe, downloaded per major version and channel.
                   Handles install and all layout maintenance. Only this one supports --wait.

    Installer      C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe
                   Already on the machine once anything from the Visual Studio family is
                   installed. Typically used for update, modify and uninstall.
                   Must be started from a different directory than the one it lives in.

  This script prints the exact command for a given workflow, with the reasoning behind the
  parameters. Nothing is executed unless you pass -Execute, so it is safe to run as
  documentation.

.PARAMETER Workflow
  Which workflow to show or run. Reference prints the full cheat sheet.

.PARAMETER Major
  Major SSMS version the command targets. Defaults to 22.

.PARAMETER Channel
  Release channel. Defaults to release.

.PARAMETER InstallPath
  Install directory. Required by update, modify and uninstall to identify the instance,
  optional on a fresh install. Defaults to the standard path for the given major version.

.PARAMETER LayoutPath
  Directory for the offline layout workflows.

.PARAMETER Add
  Workload or component IDs for the install, modify and layout workflows. Repeatable.

.PARAMETER ConfigPath
  Path to a .vsconfig file, for the Export and Import workflows.

.PARAMETER Execute
  Actually run the command instead of only printing it. Requires an elevated session.

.EXAMPLE
  .\Invoke-SsmsStubInstaller.ps1

  Prints the full reference: executables, verbs, parameters, exit codes, workload IDs, logs.

.EXAMPLE
  .\Invoke-SsmsStubInstaller.ps1 -Workflow Layout -LayoutPath D:\SSMS_Layout

  Shows how to build an offline layout, without running anything.

.EXAMPLE
  .\Invoke-SsmsStubInstaller.ps1 -Workflow Update -Execute

  Updates the installed SSMS 22 in place.

.NOTES
  Companion to Update-SqlServerManagementStudio.ps1.
  See https://www.saotn.org/windows-server/update-sql-server-management-studio-ssms-automatically/
#>
#Requires -Version 5.1

[CmdletBinding()]
param(
  [ValidateSet(
    'Reference', 'Detect', 'Install', 'Update', 'UpdateAll', 'Modify', 'Repair',
    'Uninstall', 'Export', 'Import', 'Layout', 'VerifyLayout', 'InstallFromLayout',
    'ModifySettings', 'Winget', 'Logs'
  )]
  [string]$Workflow = 'Reference',

  [ValidateRange(21, 99)]
  [int]$Major = 22,

  [ValidateSet('release', 'preview')]
  [string]$Channel = 'release',

  [string]$InstallPath,

  [string]$LayoutPath = 'C:\SSMS_Layout',

  [string[]]$Add = @('Microsoft.SqlServer.Workload.SSMS.CodeTools'),

  [string]$ConfigPath = 'C:\SSMS_Layout\ssms.vsconfig',

  [switch]$Execute
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$bootstrapperUri = "https://aka.ms/ssms/$Major/$Channel/vs_SSMS.exe"
$bootstrapper    = Join-Path -Path $env:TEMP -ChildPath 'vs_SSMS.exe'
$vsInstaller     = Join-Path -Path ${env:ProgramFiles(x86)} -ChildPath 'Microsoft Visual Studio\Installer\setup.exe'
$productId       = 'Microsoft.VisualStudio.Product.SSMS'
$channelId       = "SSMS.$Major.SSMS.Release"

if (-not $InstallPath) {
  $InstallPath = Join-Path -Path $env:ProgramFiles -ChildPath "Microsoft SQL Server Management Studio $Major\Release"
}

# Every workflow: which executable, which arguments, and why.
$workflows = [ordered]@{

  Detect = @{
    Summary = 'Find installed SSMS versions and tell the two installer generations apart.'
    Why     = @(
      'SSMS 21+ registers as "SQL Server Management Studio <major>", without the "Microsoft " prefix'
      'that SSMS 17 to 20 used, and leaves VersionMajor empty. Its UninstallString points at the'
      'Visual Studio Installer, which is the reliable way to tell the generations apart.'
      'Guard the property access: plenty of uninstall keys carry no DisplayName at all, and'
      'under Set-StrictMode -Version Latest touching an absent property is a terminating error.'
    )
    Script  = {
      $keys = @(
        'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
        'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
      )
      @(Get-ItemProperty -Path $keys -ErrorAction SilentlyContinue |
        Where-Object {
          $_.PSObject.Properties['DisplayName'] -and
          $_.DisplayName -like '*SQL Server Management Studio*'
        }) |
        Select-Object DisplayName, DisplayVersion,
          @{ n = 'InstallerType'; e = {
              if ($_.PSObject.Properties['UninstallString'] -and
                  $_.UninstallString -match 'Visual Studio\\Installer\\setup\.exe') { 'VisualStudio' } else { 'Legacy' }
            } },
          InstallLocation |
        Format-Table -AutoSize
    }
  }

  Install = @{
    Summary = 'Fresh silent install, waiting for the real exit code.'
    File    = $bootstrapper
    Args    = @('--quiet', '--wait', '--norestart')
    Why     = @(
      '--quiet suppresses all UI. --passive shows progress without prompting instead.'
      '--wait is the important one: without it the bootstrapper returns as soon as it has'
      'handed off to the Visual Studio Installer, so exit code 0 does not mean "installed".'
      '--norestart must be paired with --quiet or --passive and defers any needed reboot.'
      'Add --installPath to move it off the system drive, --add for workloads.'
    )
    Download = $true
  }

  Update = @{
    Summary = 'Update an existing installation in place. Replaces the old uninstall-then-install dance.'
    File    = $bootstrapper
    Args    = @('update', '--installPath', $InstallPath, '--quiet', '--wait', '--norestart')
    Why     = @(
      'For update, --installPath is required: it identifies which instance to act upon.'
      'You can also run this from the installer (setup.exe), but setup.exe does not support'
      '--wait, so use Start-Process -Wait there if you need a meaningful exit code.'
      'Note this updates that major version. SSMS 21 and 22 are separate products, so the'
      'SSMS 22 bootstrapper will not lift an SSMS 21 install to 22; it installs side by side.'
    )
    Download = $true
  }

  UpdateAll = @{
    Summary = 'Update every installed Visual Studio family product, in sequence.'
    File    = $vsInstaller
    Args    = @('updateall', '--quiet', '--norestart')
    Why     = @(
      'Works with --quiet and --passive only. Useful on a management server that also has'
      'Visual Studio or multiple SSMS majors installed.'
    )
  }

  Modify = @{
    Summary = 'Add or remove workloads and components on an existing installation.'
    File    = $vsInstaller
    Args    = @('modify', '--productId', $productId, '--channelId', $channelId) +
              @($Add | ForEach-Object { '--add'; $_ }) +
              @('--quiet', '--norestart')
    Why     = @(
      'modify requires --channelId together with either --productId or --installPath.'
      'Repeat --add (or --remove) per ID; a space-separated list does not work.'
      'Append ";includeRecommended" to an ID, or pass --includeRecommended globally.'
    )
  }

  Repair = @{
    Summary = 'Repair a damaged installation.'
    File    = $vsInstaller
    Args    = @('repair', '--installPath', $InstallPath, '--quiet', '--norestart')
    Why     = @('Reinstalls the payloads of the identified instance without changing the selection.')
  }

  Uninstall = @{
    Summary = 'Silently uninstall SSMS 21 or later.'
    File    = $vsInstaller
    Args    = @('uninstall', '--productId', $productId, '--channelId', $channelId, '--quiet', '--norestart')
    Why     = @(
      'This is not /uninstall /quiet. Those are the switches of the pre-21 standalone setup.'
      'The UninstallString in the registry already contains the correct verb plus an'
      '--installPath, so reading it and appending --quiet works too. What does not work is'
      'taking only the executable out of that string and inventing your own arguments.'
      'Parameter names and IDs are case-insensitive, so Product.Ssms and Product.SSMS both match.'
    )
  }

  Export = @{
    Summary = 'Export the current selection to a .vsconfig file, to reproduce it elsewhere.'
    File    = $vsInstaller
    Args    = @('export', '--installPath', $InstallPath, '--config', $ConfigPath, '--quiet')
    Why     = @('Gives you a machine-readable manifest of the installed workloads and components.')
  }

  Import = @{
    Summary = 'Install using a previously exported .vsconfig.'
    File    = $bootstrapper
    Args    = @('--config', $ConfigPath, '--quiet', '--wait', '--norestart')
    Why     = @(
      'Additive only: it never removes anything that is not in the file, and it silently'
      'ignores entries that do not apply to SSMS.'
    )
    Download = $true
  }

  Layout = @{
    Summary = 'Create an offline layout (network cache) for disconnected or managed installs.'
    File    = $bootstrapper
    Args    = @('--layout', $LayoutPath, '--lang', 'en-US') +
              @($Add | ForEach-Object { '--add'; $_ }) +
              @('--includeRecommended')
    Why     = @(
      'Layout maintenance always runs from the bootstrapper, never from setup.exe, and always'
      'starts with --layout. Leave --add out entirely to pull everything, or use --all.'
      'Re-run the same command later to refresh the layout to a newer release, and add'
      '--clean to drop the superseded packages.'
      'Only what is in the layout can be installed from it, so include everything you need.'
    )
    Download = $true
  }

  VerifyLayout = @{
    Summary = 'Check an existing layout for missing or corrupt files, and repair it.'
    # [IO.Path]::Combine, not Join-Path: Join-Path resolves through the provider and throws
    # "Cannot find drive" when the layout lives on a drive or share that is not there yet.
    File    = [System.IO.Path]::Combine($LayoutPath, 'vs_SSMS.exe')
    Args    = @('--layout', $LayoutPath, '--verify')
    Why     = @(
      'Use --verify to only report, --fix to redownload what is broken (needs internet).'
    )
  }

  InstallFromLayout = @{
    Summary = 'Install on a client from the offline layout, without touching the internet.'
    File    = [System.IO.Path]::Combine($LayoutPath, 'vs_SSMS.exe')
    Args    = @('--noWeb', '--noUpdateInstaller', '--quiet', '--wait', '--norestart')
    Why     = @(
      '--noWeb makes setup use the layout files instead of downloading payloads. It does not'
      'stop the installer from checking Microsoft servers for updates to itself, which is what'
      '--noUpdateInstaller covers. Careful: combined with --quiet, --noUpdateInstaller makes'
      'the command fail with a nonzero exit code when an installer update would be required.'
      'Run this from the layout directory.'
    )
  }

  ModifySettings = @{
    Summary = 'Point an installed instance at a different update source, or disable updates.'
    File    = $vsInstaller
    Args    = @('modifySettings', '--installPath', $InstallPath, '--newChannelUri', "https://aka.ms/ssms/$Major/$Channel/channel", '--quiet')
    Why     = @(
      'Use this to move clients onto an internal layout share, or to freeze updates by'
      'pointing --newChannelUri at a path that does not exist, for example C:\doesNotExist.chman.'
      'Add --removeOos true to persistently drop components that went out of support.'
    )
  }

  Winget = @{
    Summary = 'Let the Windows Package Manager do it.'
    File    = 'winget'
    Args    = @('install', "Microsoft.SQLServerManagementStudio.$Major")
    Why     = @(
      'Installs the core workload only. Still needs elevation for the Visual Studio Installer'
      'operations, and SSMS has to be closed for an upgrade. Handy interactively, less so in'
      'an unattended pipeline where you want control over workloads and exit codes.'
    )
  }

  Logs = @{
    Summary = 'Find the setup logs after a failure.'
    Why     = @(
      'Every operation writes logs to %TEMP%. Sort by date and look for three prefixes:'
      'ssms_bootstrapper (the stub), ssms_client (the installer app), ssms_setup (the engine).'
    )
    Script  = {
      @(Get-ChildItem -Path $env:TEMP -Filter 'ssms_*' -ErrorAction SilentlyContinue |
        Sort-Object LastWriteTime -Descending |
        Select-Object -First 15 LastWriteTime, Length, Name) |
        Format-Table -AutoSize
    }
  }
}

function Write-Heading {
  param([Parameter(Mandatory)][string]$Text)
  Write-Host ''
  Write-Host $Text -ForegroundColor Cyan
  Write-Host ('-' * $Text.Length) -ForegroundColor Cyan
}

function Show-Reference {
  Write-Heading "SSMS $Major stub installer reference"

  Write-Host 'Two executables, and they are not interchangeable:'
  Write-Host "  Bootstrapper : $bootstrapperUri"
  Write-Host "                 downloads to $bootstrapper, roughly 5 MB"
  Write-Host '                 install plus all layout maintenance, and the only one that takes --wait'
  Write-Host "  Installer    : $vsInstaller"
  Write-Host '                 update, modify, repair, uninstall, export, modifySettings'
  Write-Host '                 start it from a different directory than the one it resides in'
  Write-Host ''
  Write-Host 'Identifiers:'
  Write-Host "  productId    : $productId"
  Write-Host "  channelId    : $channelId"
  Write-Host "  installPath  : $InstallPath"
  Write-Host '  parameter names and IDs are case-insensitive'

  Write-Heading 'Verbs'
  @(
    [pscustomobject]@{ Verb = '(blank)';        Meaning = 'install the product, and all layout maintenance' }
    [pscustomobject]@{ Verb = 'update';         Meaning = 'update one installed product' }
    [pscustomobject]@{ Verb = 'updateall';      Meaning = 'update all installed products in sequence' }
    [pscustomobject]@{ Verb = 'modify';         Meaning = 'add or remove workloads and components' }
    [pscustomobject]@{ Verb = 'repair';         Meaning = 'repair an installed product' }
    [pscustomobject]@{ Verb = 'uninstall';      Meaning = 'uninstall an installed product' }
    [pscustomobject]@{ Verb = 'export';         Meaning = 'write the selection to a .vsconfig file' }
    [pscustomobject]@{ Verb = 'modifySettings'; Meaning = 'change the update source of an instance' }
    [pscustomobject]@{ Verb = 'removeChannel';  Meaning = 'drop a cached update channel' }
  ) | Format-Table -AutoSize | Out-String | Write-Host

  Write-Heading 'Parameters worth knowing'
  @(
    [pscustomobject]@{ Parameter = '--quiet';             Note = 'no UI at all' }
    [pscustomobject]@{ Parameter = '--passive';           Note = 'UI without prompts, overrides --quiet' }
    [pscustomobject]@{ Parameter = '--wait';              Note = 'return the real exit code; bootstrapper only' }
    [pscustomobject]@{ Parameter = '--norestart';         Note = 'defer a reboot; needs --quiet or --passive' }
    [pscustomobject]@{ Parameter = '--installPath <dir>'; Note = 'optional on install, required to target an instance' }
    [pscustomobject]@{ Parameter = '--add <id>';          Note = 'repeat per workload or component' }
    [pscustomobject]@{ Parameter = '--includeRecommended';Note = 'pull in recommended components of the selection' }
    [pscustomobject]@{ Parameter = '--force';             Note = 'close a running SSMS; can lose unsaved work' }
    [pscustomobject]@{ Parameter = '--noWeb';             Note = 'use layout payloads instead of downloading' }
    [pscustomobject]@{ Parameter = '--noUpdateInstaller'; Note = 'do not self-update the installer' }
    [pscustomobject]@{ Parameter = '--addProductLang';    Note = 'language pack, for example nl is not offered; en-us is' }
    [pscustomobject]@{ Parameter = '--config <file>';     Note = 'apply a .vsconfig selection, additive only' }
  ) | Format-Table -AutoSize -Wrap | Out-String | Write-Host

  Write-Heading 'Workload and component IDs'
  Write-Host '  Microsoft.SqlServer.Workload.SSMS.CodeTools            Git integration and code tools'
  Write-Host '  Microsoft.SqlServer.Workload.SSMS.BI                   Business Intelligence'
  Write-Host '  Microsoft.SqlServer.Workload.SSMS.AI                   Copilot in SSMS'
  Write-Host '  Microsoft.SqlServer.Workload.SSMS.HybridAndMigration   assessment and migration'
  Write-Host '  Microsoft.SSMS.Component.IS / .RS / .AS                Integration, Reporting, Analysis Services'
  Write-Host '  Microsoft.Component.HelpViewer                         offline help'

  Write-Heading 'Exit codes'
  Write-Host '  0            success'
  Write-Host '  1641         success, reboot was initiated'
  Write-Host '  3010         success, reboot required before use'
  Write-Host '  740          elevation required'
  Write-Host '  1001         Visual Studio installer already running'
  Write-Host '  1003 / 8006  SSMS is in use'
  Write-Host '  1602 / 5004  canceled'
  Write-Host '  1618         another installation running'
  Write-Host '  5003         bootstrapper could not download the installer'
  Write-Host '  5005         command-line parse error'
  Write-Host '  5007         blocked, machine does not meet the requirements'
  Write-Host '  8010         operating system not supported'
  Write-Host '  -1073720687  connectivity failure'
  Write-Host '  anything else, including 1 and 1603: check the logs'
  Write-Host ''
  Write-Host 'Treat 0, 1641 and 3010 as success. Everything else is a failure.'

  Write-Heading 'Workflows in this script'
  foreach ($name in $workflows.Keys) {
    Write-Host ("  {0,-18} {1}" -f $name, $workflows[$name].Summary)
  }
  Write-Host ''
  Write-Host "Show one with:  .\Invoke-SsmsStubInstaller.ps1 -Workflow <name>"
  Write-Host "Run one with:   .\Invoke-SsmsStubInstaller.ps1 -Workflow <name> -Execute"
  Write-Host ''
}

function Get-Bootstrapper {
  param([Parameter(Mandatory)][string]$Destination)

  Write-Host "Downloading $bootstrapperUri"
  $previous = $ProgressPreference
  $ProgressPreference = 'SilentlyContinue'
  try {
    Invoke-WebRequest -Uri $bootstrapperUri -OutFile $Destination -UseBasicParsing
  }
  finally {
    $ProgressPreference = $previous
  }
  Write-Host "Saved to $Destination"
}

function Test-Elevated {
  $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
  return ([Security.Principal.WindowsPrincipal]$identity).IsInRole(
    [Security.Principal.WindowsBuiltInRole]::Administrator)
}

# ---------------------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------------------

if ($Workflow -eq 'Reference') {
  Show-Reference
  return
}

$definition = $workflows[$Workflow]

Write-Heading "$Workflow - $($definition.Summary)"

if ($definition.Contains('Why')) {
  foreach ($line in $definition.Why) {
    Write-Host "  $line" -ForegroundColor DarkGray
  }
  Write-Host ''
}

# Workflows that are pure PowerShell rather than a setup command.
if ($definition.Contains('Script')) {
  if ($Execute) {
    & $definition.Script | Out-String | Write-Host
  }
  else {
    Write-Host 'PowerShell:'
    Write-Host ($definition.Script.ToString().Trim())
    Write-Host ''
    Write-Host 'Add -Execute to run it.' -ForegroundColor Yellow
  }
  return
}

$commandLine = '"{0}" {1}' -f $definition.File, ($definition.Args -join ' ')
Write-Host 'Command:'
Write-Host "  $commandLine"
Write-Host ''

if (-not $Execute) {
  if ($definition.Contains('Download') -and $definition.Download) {
    Write-Host "Needs the bootstrapper at $bootstrapper first (downloaded from $bootstrapperUri)." -ForegroundColor DarkGray
  }
  Write-Host 'Add -Execute to run it.' -ForegroundColor Yellow
  return
}

if (-not (Test-Elevated)) {
  throw 'This workflow needs an elevated session. Visual Studio Installer operations require administrator rights.'
}

if ($definition.Contains('Download') -and $definition.Download -and -not (Test-Path -LiteralPath $definition.File)) {
  Get-Bootstrapper -Destination $definition.File
}

if (-not (Test-Path -LiteralPath $definition.File) -and $definition.File -ne 'winget') {
  throw "Not found: $($definition.File)"
}

$process = Start-Process -FilePath $definition.File -ArgumentList $definition.Args -Wait -PassThru
$code = $process.ExitCode

if (@(0, 1641, 3010) -contains $code) {
  Write-Host "Exit code $code (success)."
}
else {
  Write-Warning "Exit code $code. Check %TEMP% for ssms_bootstrapper*, ssms_client* and ssms_setup* logs."
}

Conclusion

In this post I shared a "fire-and-forget" PowerShell script designed to uninstall any existing version of SSMS and then download and silently install the latest release from Microsoft. This makes the process ideal for fully automated, unattended deployments - especially useful when you're provisioning new SQL Server instances or management servers.

In addition, you can combine this script with command-line parameters to further customize installation behavior, leveraging Microsoft's documented SSMS installer options.

Note: Claude assisted in the updated scripts.

I write these posts in my spare time, based on real problems from my day job as a sysadmin. If this one saved you some debugging time, a small donation is much appreciated. Thanks! 🙏

Leave a Comment