A few years ago I wrote an App-V recipe for SSMS 18.5. For MSIX, the older SSMS versions (up to 19/20) also worked well: unpack the package and fix the permissions of the registry keys in User.dat, as described in the Tech Community article [FIXED] Azure Virtual Desktop (AVD) – Packaging the SQL Server Management Studio (SSMS) to MSIX. In the MSIXForcelets this is Add-MSIXFixSSMS.
Then Microsoft switched SSMS to the Visual Studio Installer. SSMS 21 and 22 are built on the Visual Studio shell – and the old recipe does not work anymore. The capture with the MSIX Packaging Tool works, the package installs, SSMS starts – and then this:
Error opening the connection dialog. Element not found. (Exception from HRESULT: 0x80070490) (Microsoft.VisualStudio.Data.Tools.Design.Core)
No connect dialog, no Object Explorer, no "New Query". The cause has nothing to do with registry permissions, so the new recipe is something completely different.
We have been looking for a fix ever since SSMS moved to the Visual Studio Installer. For many years we have kept the flag flying for a virtualized SSMS – first with App-V, then with MSIX. So we are especially happy that it finally works again: SSMS 22 as an MSIX package, with connect dialog, Object Explorer and queries.
| Application | SQL Server Management Studio 22 |
|---|---|
| Version | 22.10.2 (build 22.10.12217.157) |
| Packaging | MSIX Packaging Tool |
| Tested on | Windows Server 2025, SQL Server 2025 Express |
| Tools | MSIX Packaging Tool (Package Editor), PowerShell, Process Monitor |
SSMS 22 is installed with the Visual Studio Installer. I build the package with the MSIX Packaging Tool from an offline layout:
C:\Program Files\Microsoft SQL Server Management Studio 22\Release. The SSMS files then land in the package root instead of VFS\ProgramFilesX64. As installer choose the install script C:\Install\Install-SSMS22.ps1 (below) – the MSIX Packaging Tool runs PowerShell scripts directly. Use the version from the download script as package version. Important: the restart must be prevented. The script installs with --norestart and reports exit code 3010 (restart required) as success – otherwise the MSIX Packaging Tool restarts the machine in the middle of the capture.param(
[string] $LayoutPath = 'C:\Install\SSMS22',
[string[]] $Languages = @('en-US', 'de-DE')
)
$ProgressPreference = 'SilentlyContinue'
$LayoutPath = $LayoutPath.TrimEnd('\') # a trailing backslash would escape the closing quote of --layout "..."
New-Item -ItemType Directory -Path $LayoutPath -Force | Out-Null
# Download the current bootstrapper to %TEMP%, not into the layout. If the layout update
# fails, the layout keeps the bootstrapper that matches its payloads.
$bootstrapper = Join-Path $env:TEMP 'vs_SSMS.exe'
Invoke-WebRequest -Uri 'https://aka.ms/ssms/22/release/vs_SSMS.exe' -OutFile $bootstrapper -UseBasicParsing
# Create or update the offline layout
$arguments = "--layout `"$LayoutPath`" --wait --quiet " + (($Languages | ForEach-Object { "--lang $_" }) -join ' ')
$process = Start-Process -FilePath $bootstrapper -ArgumentList $arguments -Wait -PassThru
if ($process.ExitCode -notin 0, 3010) {
Write-Warning "Layout update failed (exit code $($process.ExitCode)), using the existing layout."
}
# The package version comes from the layout, not from the online channel
(Get-Content (Join-Path $LayoutPath 'ChannelManifest.json') -Raw | ConvertFrom-Json).info.buildVersion
Two details from practice:
ChannelManifest.json in the layout, not from the online channel. If the layout update failed, the channel is newer than what you actually install.param(
[string] $LayoutPath = 'C:\Install\SSMS22'
)
# Root certificates from the layout (needed on machines without internet access)
Get-ChildItem "$LayoutPath\certificates" -Filter *.cer -ErrorAction SilentlyContinue |
ForEach-Object { certutil.exe -addstore -f Root $_.FullName | Out-Null }
# Silent installation from the offline layout - with --noWeb nothing is downloaded
$arguments = '--noWeb --quiet --norestart --wait --addProductLang de-DE --includeRecommended'
$process = Start-Process -FilePath (Join-Path $LayoutPath 'vs_SSMS.exe') -ArgumentList $arguments -Wait -PassThru
# 3010 = restart required. Report it as success, otherwise the MSIX Packaging Tool
# restarts the machine in the middle of the capture.
if ($process.ExitCode -notin 0, 3010) {
throw "SSMS setup failed with exit code $($process.ExitCode), see $env:TEMP\dd_*.log"
}
exit 0
Why a script and not vs_SSMS.exe directly in the MSIX Packaging Tool: the setup can return 3010 (restart required), even with --norestart. In my case it came from the Visual C++ runtime, which the setup updates – the packaging machine had an older runtime whose DLLs were in use. The MSIX Packaging Tool takes this literally, restarts the machine during the capture and continues after the next logon. The script reports 3010 as success, and the capture runs through without a restart. (Alternatively, add 3010 as an installer exit code without restart in the settings of the MSIX Packaging Tool.)
Watch out for the execution policy on the packaging machine: with the Windows Server default Restricted, the script does not run at all – and the MSIX Packaging Tool still reports success and creates an empty package of a few KB. Set Set-ExecutionPolicy RemoteSigned on the packaging machine before the capture.
The connect dialog asks the Visual Studio Setup API which VS instance the running SSMS.exe belongs to. The API reads %ProgramData%\Microsoft\VisualStudio\Packages\_Instances\<id>\state.json – and in the package this file is wrong or missing. A fresh capture contains it, but its installationPath points to the installation folder on the packaging machine, C:\Program Files\Microsoft SQL Server Management Studio 22\Release, not to the MSIX location. And ProgramData\Microsoft\VisualStudio\Packages also holds the installer cache (about 870 MB), so it is a typical candidate for a cleanup step – and then the instance information is gone completely.
The API only checks that the path of SSMS.exe is below installationPath. So C:\Program Files\WindowsApps is enough – independent of version and publisher, the fix survives re-signing and updates. All data for the state.json (instance ID, product, version, channel) is still in the package: the VS Installer leaves a copy of the channel manifest and the catalog under VFS\Local AppData\Microsoft\VisualStudio\Packages\_Channels.
Everything happens in the Package Editor of the MSIX Packaging Tool. Only the state.json is prepared beforehand – with an editor or with a few lines of PowerShell.
This is the file I tested with SSMS 22.10.2:
{
"installationName": "SSMS/22.10.2+12217.157",
"installationPath": "C:\\Program Files\\WindowsApps",
"launchParams": {
"fileName": "Common7\\IDE\\SSMS.exe",
"arguments": ""
},
"installationVersion": "22.10.12217.157",
"installDate": "2026-10-13T06:04:16Z",
"updateDate": "2026-10-13T06:04:16Z",
"layoutPath": null,
"channelId": "SSMS.22.SSMS.Release",
"channelPath": "C:\\ProgramData\\Microsoft\\VisualStudio\\Packages\\_Channels\\4044bd4c\\catalog.json",
"channelUri": "https://aka.ms/ssms/22/release/channel",
"installChannelUri": "https://aka.ms/ssms/22/release/channel",
"localizedResources": [
{ "language": "en-us", "title": "SQL Server Management Studio 22", "description": "", "license": "" }
],
"product": {
"id": "Microsoft.VisualStudio.Product.Ssms",
"version": "22.10.12217.157",
"chip": "x64",
"type": "Product",
"isExtension": false
},
"selectedPackages": [
{
"id": "Microsoft.VisualStudio.Product.Ssms",
"version": "22.10.12217.157",
"chip": "x64",
"type": "Product",
"selectedState": "IndividuallySelected",
"userSelected": true
}
],
"catalogInfo": {
"id": "SSMS/22.10.2+12217.157",
"buildBranch": "stable",
"buildVersion": "22.10.12217.157",
"includeBuildVersionInDisplayVersion": "False",
"localBuild": "build-lab",
"manifestName": "SSMS",
"manifestType": "installer",
"productDisplayVersion": "22.10.2",
"productLine": "SSMS22",
"productLineVersion": "22",
"productMilestone": "RTW",
"productMilestoneIsPreRelease": "False",
"productName": "SQL Server Management Studio",
"productPatchVersion": "2",
"productPreReleaseMilestoneSuffix": "1.0",
"productRelease": "RTW",
"productSemanticVersion": "22.10.2+12217.157"
},
"properties": {
"channelManifestId": "SSMS.22.SSMS.Release/22.10.2+12217.157",
"nickname": "",
"setupEngineFilePath": "C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\setup.exe"
}
}
The important value is installationPath. For another SSMS version, take the other values from the two files in the package under VFS\Local AppData\Microsoft\VisualStudio\Packages\_Channels\<channel>\:
| state.json | Source |
|---|---|
installationName, catalogInfo | catalog.json → info.id, info |
installationVersion, product.version, selectedPackages.version | channelManifest.json → channelItems, entry with type = ChannelProduct → version |
channelId | channelManifest.json → info.manifestName |
properties.channelManifestId | channelManifest.json → info.id |
channelUri, installChannelUri | channelManifest.json → updateUri |
channelPath | folder name of the channel (4044bd4c) |
catalog.json has 18 MB – no fun in Notepad. PowerShell reads the values directly from the MSIX file, without unpacking, and writes the state.json:
$msix = 'C:\Packages\SSMS22-x64-22.10.12217.157.msix'
$stateFile = 'C:\Packages\state.json'
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zip = [IO.Compression.ZipFile]::OpenRead($msix)
function Read-ZipJson($pattern) {
$entry = $zip.Entries | Where-Object { [uri]::UnescapeDataString($_.FullName) -like $pattern } | Select-Object -First 1
$reader = New-Object IO.StreamReader($entry.Open())
try { $reader.ReadToEnd() | ConvertFrom-Json } finally { $reader.Close() }
}
$vs = 'VFS/Local AppData/Microsoft/VisualStudio/Packages'
$instanceId = ($zip.Entries | Where-Object { [uri]::UnescapeDataString($_.FullName) -like "$vs/_Instances/*" } | Select-Object -First 1).FullName.Split('/')[-2]
$channelId = ($zip.Entries | Where-Object { [uri]::UnescapeDataString($_.FullName) -like "$vs/_Channels/*/catalog.json" } | Select-Object -First 1).FullName.Split('/')[-2]
$channel = Read-ZipJson "$vs/_Channels/*/channelManifest.json"
$catalog = Read-ZipJson "$vs/_Channels/*/catalog.json"
$zip.Dispose()
$product = $channel.channelItems | Where-Object type -eq 'ChannelProduct' | Select-Object -First 1
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
$state = [ordered]@{
installationName = $catalog.info.id
installationPath = 'C:\Program Files\WindowsApps'
launchParams = [ordered]@{ fileName = 'Common7\IDE\SSMS.exe'; arguments = '' }
installationVersion = $product.version
installDate = $now
updateDate = $now
layoutPath = $null
channelId = $channel.info.manifestName
channelPath = "C:\ProgramData\Microsoft\VisualStudio\Packages\_Channels\$channelId\catalog.json"
channelUri = $channel.updateUri
installChannelUri = $channel.updateUri
localizedResources = @([ordered]@{ language = 'en-us'; title = "$($catalog.info.productName) $($catalog.info.productLineVersion)"; description = ''; license = '' })
product = [ordered]@{ id = $product.id; version = $product.version; chip = 'x64'; type = 'Product'; isExtension = $false }
selectedPackages = @([ordered]@{ id = $product.id; version = $product.version; chip = 'x64'; type = 'Product'; selectedState = 'IndividuallySelected'; userSelected = $true })
catalogInfo = $catalog.info
properties = [ordered]@{
channelManifestId = $channel.info.id
nickname = ''
setupEngineFilePath = 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe'
}
}
[IO.File]::WriteAllText($stateFile, ($state | ConvertTo-Json -Depth 5), (New-Object Text.UTF8Encoding $false))
"Instance folder: $instanceId"
The script also prints the instance ID you need in step 3 – in my package cd2d802a.
Start the MSIX Packaging Tool, choose Package editor and open the captured .msix.
Delete these folders (right-click → Delete):
| Folder | Why |
|---|---|
VFS\Windows\assembly\NativeImages_v4.0.30319_64 | NGEN native images of the packaging machine, about 1 GB. They only match the .NET version of that machine and are never used in the container. |
VFS\Local AppData\.IdentityService | Account stores of the user who captured the package |
VFS\Common AppData\Package Cache | Installer payload of the VS Installer |
VFS\Common AppData\Microsoft\VisualStudio\Packages | Installer cache of the VS Installer, about 870 MB. Also contains the state.json with the wrong path – it is added again in the next step. |
Then add the state.json here:
VFS\Common AppData\Microsoft\VisualStudio\Packages\_Instances\<id>\state.json
<id> is the folder name under VFS\Local AppData\Microsoft\VisualStudio\Packages\_Instances. Create the missing folders with right-click → New folder and add the file with right-click → Add file. The state.json under Local AppData only contains {} and is not the one the Setup API reads – leave it alone.
Delete these two keys (right-click → Delete). They contain the NGEN data of the packaging machine and would hide the real values of the target system:
MACHINE\SOFTWARE\Microsoft\.NETFramework MACHINE\SOFTWARE\Wow6432Node\Microsoft\.NETFramework
The VS Installer installs the Windows App Runtime during the capture, and the MSIX Packaging Tool turns it into a dependency. SSMS does not need it. Under Manifest file, click Open file, delete this line in <Dependencies>, save and close Notepad, and confirm the change:
<PackageDependency Name="Microsoft.WindowsAppRuntime.1.4" MinVersion="4000.1010.1349.0" Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US" />
Under Package information, choose your certificate in the signing preference. The publisher must match the subject of the certificate. Then Save the package.
The connect dialog opens, Object Explorer and queries work, without the Windows App Runtime installed. The package shrinks from 2 GB (fresh capture) to 821 MB. I tested the whole recipe with a capture on a clean Windows Server 2025 (remote conversion of the MSIX Packaging Tool) and installed the package on a second machine.
All of the above, except signing, is one line with the MSIXForcelets. Install the module once from the PowerShell Gallery:
Install-Module MSIXForcelets
Then:
Add-MSIXFixSSMS22 -MsixFile 'C:\Packages\SSMS22-x64-22.10.12217.157.msix' -OutputFilePath 'C:\Packages\SSMS22-x64-22.10.12217.157_fixed.msix'
The function reads instance and channel data from the package itself, so you need no other parameters. Optional:
-Subject 'CN=...' / -Version '22.10.12217.158' – change publisher or version in the same step-InstallationRoot 'D:\WindowsApps' – packages installed on another volume-KeepWindowsAppRuntimeDependency – keep the WindowsAppRuntime dependencyThen sign the package as usual, e.g. with Set-MSIXSignature.
Comments