Migrating Shell Extensions from Windows 10 to Windows 11: The Complete Developer Guide
Updated February 2026 — Covers Windows 11 24H2, MSIX App Attach, Win32 App Isolation
If you maintain a Windows desktop application that includes Shell Extensions — context menu handlers, icon overlays, property sheet extensions, or preview handlers — you have likely encountered issues after users upgraded to Windows 11. Extensions that worked flawlessly for years on Windows 10 may now cause crashes, fail to appear in the new context menu, or trigger security warnings.
Windows 11 fundamentally changed how the context menu works, introduced new security boundaries, and is pushing developers toward modern packaging with MSIX. This guide covers everything you need to know to migrate your shell extensions and ensure they work reliably on Windows 11, while maintaining backward compatibility with Windows 10.
What Changed in Windows 11
1. The Two-Tier Context Menu
The most visible change is the new context menu. In Windows 10, right-clicking a file showed a single menu containing all entries from registered IContextMenu handlers. In Windows 11, the menu is split:
- Primary menu: Shows a compact, modern menu using the IExplorerCommand interface.
- “Show more options”: Reveals the legacy menu with all
IContextMenuentries.
Key implication: If your extension only implements IContextMenu, your menu items are hidden behind “Show more options” by default. Users will not see them unless they click through.
2. IExplorerCommand Interface
The modern Windows 11 context menu uses IExplorerCommand, IExplorerCommandState, and IEnumExplorerCommand. These interfaces provide:
- Asynchronous loading: Menu items load without blocking the UI thread
- Built-in icon support: SVG and modern icon formats
- Integrated state management: Enable/disable/hide items based on selection
- Sub-menu support: Via
IEnumExplorerCommand
3. MSIX Packaging Integration
Windows 11 strongly encourages (and in some cases requires) shell extension registration through MSIX manifests via the desktop4:FileExplorerContextMenus and desktop10:ApprovedShellExtension elements.
4. Win32 App Isolation (Preview)
Starting in Windows 11 24H2, Microsoft introduced Win32 App Isolation, which runs Win32 applications in a sandboxed environment similar to UWP. Shell extensions loading into explorer.exe under this model face additional restrictions.
Strategy Decision: Migrate, Dual-Target, or Stay Legacy?
Before coding, decide your approach:
| Strategy | When to Use | Effort | Result |
|---|---|---|---|
| Full Migration to IExplorerCommand | New apps, small menu | Medium | Items in primary menu, modern feel |
| Dual Registration (both interfaces) | Existing apps needing Win10+11 | High | Works everywhere, future-proof |
| Stay Legacy (IContextMenu only) | Legacy apps, Win10 focus | None | Items hidden behind “Show more options” |
| Sparse Package + IExplorerCommand | Apps without full MSIX | Medium | Primary menu without full MSIX |
Option 1: Implementing IExplorerCommand
Interface Overview
class ATL_NO_VTABLE CMyExplorerCommand :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CMyExplorerCommand, &CLSID_MyExplorerCommand>,
public IExplorerCommand,
public IObjectWithSite
{
public:
// IExplorerCommand
STDMETHOD(GetTitle)(IShellItemArray* psiItemArray, LPWSTR* ppszName);
STDMETHOD(GetIcon)(IShellItemArray* psiItemArray, LPWSTR* ppszIcon);
STDMETHOD(GetToolTip)(IShellItemArray* psiItemArray, LPWSTR* ppszInfotip);
STDMETHOD(GetCanonicalName)(GUID* pguidCommandName);
STDMETHOD(GetState)(IShellItemArray* psiItemArray, BOOL fOkToBeSlow,
EXPCMDSTATE* pCmdState);
STDMETHOD(Invoke)(IShellItemArray* psiItemArray, IBindCtx* pbc);
STDMETHOD(GetFlags)(EXPCMDFLAGS* pFlags);
STDMETHOD(EnumSubCommands)(IEnumExplorerCommand** ppEnum);
};
Implementation Example
STDMETHODIMP CMyExplorerCommand::GetTitle(
IShellItemArray* psiItemArray, LPWSTR* ppszName
) {
// Return the menu item text
return SHStrDupW(L"Open with My App", ppszName);
}
STDMETHODIMP CMyExplorerCommand::GetIcon(
IShellItemArray* psiItemArray, LPWSTR* ppszIcon
) {
// Return path to icon resource
// Supports .ico, .png, and resource references
wchar_t modulePath[MAX_PATH];
GetModuleFileName(_AtlBaseModule.GetModuleInstance(),
modulePath, MAX_PATH);
// Reference icon by resource ID
wchar_t iconPath[MAX_PATH + 10];
swprintf_s(iconPath, L"%s,-%d", modulePath, IDI_MYICON);
return SHStrDupW(iconPath, ppszIcon);
}
STDMETHODIMP CMyExplorerCommand::GetState(
IShellItemArray* psiItemArray, BOOL fOkToBeSlow,
EXPCMDSTATE* pCmdState
) {
*pCmdState = ECS_ENABLED; // Always enabled
// Optionally check file types
if (psiItemArray) {
DWORD count = 0;
psiItemArray->GetCount(&count);
if (count == 0) {
*pCmdState = ECS_HIDDEN;
}
}
return S_OK;
}
STDMETHODIMP CMyExplorerCommand::Invoke(
IShellItemArray* psiItemArray, IBindCtx* pbc
) {
if (!psiItemArray) return E_INVALIDARG;
DWORD count = 0;
HRESULT hr = psiItemArray->GetCount(&count);
if (FAILED(hr)) return hr;
for (DWORD i = 0; i < count; i++) {
ComPtr<IShellItem> pItem;
hr = psiItemArray->GetItemAt(i, &pItem);
if (FAILED(hr)) continue;
LPWSTR filePath = nullptr;
hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &filePath);
if (SUCCEEDED(hr) && filePath) {
// Process the file
ProcessFile(filePath);
CoTaskMemFree(filePath);
}
}
return S_OK;
}
STDMETHODIMP CMyExplorerCommand::GetFlags(EXPCMDFLAGS* pFlags) {
*pFlags = ECF_DEFAULT;
return S_OK;
}
STDMETHODIMP CMyExplorerCommand::EnumSubCommands(
IEnumExplorerCommand** ppEnum
) {
*ppEnum = nullptr;
return E_NOTIMPL; // No sub-commands
}
Registration for IExplorerCommand
IExplorerCommand-based extensions must be registered via package manifest (MSIX) or Sparse Package. They cannot be registered via traditional registry entries alone.
Option 2: Dual Registration (Recommended for Most Apps)
The best approach for existing applications is to implement both IContextMenu (legacy) and IExplorerCommand (modern), allowing the extension to work on both Windows 10 and Windows 11.
Architecture
MyExtension.dll
├── CMyContextMenu // IContextMenu + IShellExtInit
│ └── Registered via HKCR\*\shellex\ContextMenuHandlers
│ (Works on Win10 + Win11 "Show more options")
│
└── CMyExplorerCommand // IExplorerCommand
└── Registered via MSIX manifest
(Appears in Win11 primary menu)
Class Factory
Both COM classes live in the same DLL and are registered via the same DllGetClassObject:
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv) {
if (rclsid == CLSID_MyContextMenu) {
// Legacy handler for Win10 compatibility
return CMyContextMenuFactory::CreateInstance(riid, ppv);
}
if (rclsid == CLSID_MyExplorerCommand) {
// Modern handler for Win11 primary menu
return CMyExplorerCommandFactory::CreateInstance(riid, ppv);
}
return CLASS_E_CLASSNOTAVAILABLE;
}
Avoiding Duplicate Entries
On Windows 11, if both handlers are active, the user may see the menu item twice (once in the primary menu, once in “Show more options”). To prevent this:
- In
CMyContextMenu::QueryContextMenu(), check the Windows version:
STDMETHODIMP CMyContextMenu::QueryContextMenu(
HMENU hmenu, UINT indexMenu, UINT idCmdFirst,
UINT idCmdLast, UINT uFlags
) {
// Skip legacy registration on Win11 if the IExplorerCommand
// version is registered
if (IsWindows11OrGreater() && IsExplorerCommandRegistered()) {
return MAKE_HRESULT(SEVERITY_SUCCESS, 0, 0); // Add 0 items
}
// Normal Win10 behavior
InsertMenu(hmenu, indexMenu, MF_STRING | MF_BYPOSITION,
idCmdFirst, L"Open with My App");
return MAKE_HRESULT(SEVERITY_SUCCESS, 0, 1);
}
Option 3: Sparse Packages (Modern Registration Without Full MSIX)
If converting your entire application to MSIX is not feasible, you can use a Sparse Package to register just the IExplorerCommand handler:
Creating a Sparse Package Manifest
Create AppxManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4"
xmlns:desktop10="http://schemas.microsoft.com/appx/manifest/desktop/windows10/10"
IgnorableNamespaces="desktop4 desktop10">
<Identity Name="MyApp.ShellExtension"
Publisher="CN=My Company"
Version="1.0.0.0" />
<Properties>
<DisplayName>My App Shell Extension</DisplayName>
<PublisherDisplayName>My Company</PublisherDisplayName>
</Properties>
<Applications>
<Application Id="App">
<Extensions>
<desktop4:Extension Category="windows.fileExplorerContextMenus">
<desktop4:FileExplorerContextMenus>
<desktop4:ItemType Type="*">
<desktop4:Verb Id="MyAppOpen"
Clsid="{YOUR-EXPLORER-COMMAND-CLSID}" />
</desktop4:ItemType>
</desktop4:FileExplorerContextMenus>
</desktop4:Extension>
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:SurrogateServer DisplayName="My App Explorer Command">
<com:Class Id="{YOUR-EXPLORER-COMMAND-CLSID}"
Path="MyExtension.dll"
ThreadingModel="STA" />
</com:SurrogateServer>
</com:ComServer>
</com:Extension>
</Extensions>
</Application>
</Applications>
</Package>
Registering the Sparse Package
# Register the sparse package from your installer
Add-AppxPackage -Path "AppxManifest.xml" -ExternalLocation "C:\Program Files\MyApp" -Register
Unregistering
Remove-AppxPackage "MyApp.ShellExtension_1.0.0.0_x64__publisher-id"
Handling Windows 11 24H2 Breaking Changes
Windows 11 24H2 introduced several changes that affect shell extensions:
1. Enhanced DLL Loading Security
24H2 enforces stricter DLL loading policies:
- DLLs must be signed or catalog-verified
- DLLs from user-writable directories may be blocked
- Fix: Ensure your DLL is properly code-signed with an EV certificate
2. Explorer Process Changes
24H2 may run Explorer tabs in separate processes:
- Shell extensions loaded in one tab may not be visible in another
- Global state shared between extension instances may break
- Fix: Use shared memory or named pipes instead of global variables
3. Updated Context Menu Rendering
The context menu rendering pipeline was updated, which may cause:
- Icons not displaying correctly (wrong size, blurry)
- Custom-drawn menu items rendering incorrectly
- Fix: Use standard HBITMAP with 32-bit ARGB for menu icons. Avoid owner-drawn items.
Testing Your Migration
Test Matrix
| Test Case | Windows 10 22H2 | Windows 11 23H2 | Windows 11 24H2 |
|---|---|---|---|
| Legacy menu items visible | Primary menu | ”Show more options" | "Show more options” |
| IExplorerCommand items visible | N/A (not supported) | Primary menu | Primary menu |
| Icon overlays | ✓ | ✓ (check 15-limit) | ✓ (check 15-limit) |
| Preview handlers | ✓ | ✓ | ✓ |
| Copy hook handlers | ✓ | ✓ | ✓ |
| DLL loads without errors | ✓ | ✓ | Check signing |
Automated Testing
Create a PowerShell test script that verifies registration:
# Verify legacy IContextMenu registration
$legacyPath = "Registry::HKCR\*\shellex\ContextMenuHandlers\MyApp"
if (Test-Path $legacyPath) {
Write-Host "✓ Legacy handler registered" -ForegroundColor Green
} else {
Write-Host "✗ Legacy handler missing" -ForegroundColor Red
}
# Verify IExplorerCommand registration (via sparse package)
$package = Get-AppxPackage | Where-Object { $_.Name -match "MyApp.ShellExtension" }
if ($package) {
Write-Host "✓ Sparse package registered: $($package.Version)" -ForegroundColor Green
} else {
Write-Host "✗ Sparse package not found" -ForegroundColor Yellow
}
# Verify COM class registration
$clsid = "{YOUR-EXPLORER-COMMAND-CLSID}"
$comPath = "Registry::HKCR\CLSID\$clsid\InprocServer32"
if (Test-Path $comPath) {
$dllPath = (Get-ItemProperty $comPath)."(default)"
Write-Host "✓ COM class registered: $dllPath" -ForegroundColor Green
# Check DLL signature
$sig = Get-AuthenticodeSignature $dllPath
if ($sig.Status -eq "Valid") {
Write-Host " ✓ DLL is signed by: $($sig.SignerCertificate.Subject)" -ForegroundColor Green
} else {
Write-Host " ✗ DLL signature: $($sig.Status)" -ForegroundColor Red
}
} else {
Write-Host "✗ COM class not registered" -ForegroundColor Red
}
Debugging Tips
- Visual Studio: Attach to
explorer.exefor legacy handlers,prevhost.exefor preview handlers. - WinDbg: Use
!comcallsto trace COM interface calls in explorer. - Process Monitor: Watch for
RegQueryValuefailures that indicate missing registrations. - DebugView: Add
OutputDebugStringcalls to trace your extension’s execution path.
Migration Checklist
Use this checklist when migrating your shell extension:
- Audit current extension types (context menu, icon overlay, preview, copy hook)
- Choose migration strategy (full migration, dual registration, or stay legacy)
- Implement IExplorerCommand if targeting the Win11 primary menu
- Create sparse package manifest for IExplorerCommand registration
- Add version check in legacy IContextMenu to prevent duplicate entries
- Update code signing to meet 24H2 requirements (EV certificate recommended)
- Test on all target Windows versions (Win10 22H2, Win11 23H2, Win11 24H2)
- Update installer to register both legacy keys and sparse package
- Update uninstaller to clean up both registrations
- Document breaking changes for your users
Summary
Migrating shell extensions from Windows 10 to Windows 11 requires understanding the new two-tier context menu, IExplorerCommand interface, MSIX/Sparse Package registration, and 24H2 security changes. For most applications, dual registration (both IContextMenu and IExplorerCommand) provides the best experience across all Windows versions while future-proofing for upcoming changes.