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.
Recipe overview
| 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 |
Packaging
SSMS 22 is installed with the Visual Studio Installer. I build the package with the MSIX Packaging Tool from an offline layout:
- Offline layout. The script below creates or updates the layout with the languages you need and returns the version for the package.
- Capture with the MSIX Packaging Tool, PVAD style. Set the installation location to
C:\Program Files\Microsoft SQL Server Management Studio 22\Release. The SSMS files then land in the package root instead ofVFS\ProgramFilesX64. As installer choose the install scriptC:\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--norestartand reports exit code 3010 (restart required) as success – otherwise the MSIX Packaging Tool restarts the machine in the middle of the capture. - Fix and clean up the captured package – step by step below, or with one line of PowerShell at the end.
Download script: offline layout
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:
- Bootstrapper in %TEMP%. The fresh bootstrapper must not overwrite the one in the layout. If the layout update fails, a new bootstrapper next to old payloads breaks the installation from the layout as well.
- Version from the layout. Take the package version from
ChannelManifest.jsonin the layout, not from the online channel. If the layout update failed, the channel is newer than what you actually install.
Install script: Install-SSMS22.ps1
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 cause
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.
The fix step by step
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.
Step 1: Prepare the state.json
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.
Step 2: Open the package
Start the MSIX Packaging Tool, choose Package editor and open the captured .msix.
Step 3: Package files – clean up and add the state.json
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.
Step 4: Virtual registry – delete the .NETFramework keys
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
Step 5: Manifest file – remove the WindowsAppRuntime dependency
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" />
Step 6: Sign and save
Under Package information, choose your certificate in the signing preference. The publisher must match the subject of the certificate. Then Save the package.
Result
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.
The easy way: one line with the MSIXForcelets
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 dependency
Then sign the package as usual, e.g. with Set-MSIXSignature.
Links
- MSIXForcelets: https://msixforcelets.nick-it.de – GitHub
- Fix for older SSMS versions (up to 19/20): [FIXED] Azure Virtual Desktop (AVD) – Packaging the SQL Server Management Studio (SSMS) to MSIX
- My older App-V recipe: SQL Server Management Studio 18.5 (SSMS) App-V Recipe