Windows Shell Integration Today — Context Menus, File Associations, and What Changed in Windows 11
· Go Komura · Windows, Shell Extensions, Context Menu, File Association, COM, Windows 11, File Explorer, MSIX, Windows Development
I was consulted with “we replaced the PCs with Windows 11, and the context menu of the app you built for us years ago disappeared”. Listening more carefully, it had not disappeared. Right-click a file, choose “Show more options” at the bottom of the menu, and the familiar menu appears just as it always did. In other words, the in-house app’s menu items had been hidden one click further in. From the field we hear “it is one extra click” and “inquiries that they cannot find the item have increased”.
This is neither a failure nor a misconfiguration; it is a Windows 11 design change. File Explorer’s context menu became a two-layer structure, old and new, and the conditions for putting an item on the new menu became a completely different thing from before.
Meanwhile, the file-association and shell-extension machinery underneath is still the old world of COM and the registry. An extension key points at a ProgID, the ProgID’s verb holds a command line, and a more involved extension runs as an in-process COM server (DLL) loaded into Explorer — that structure has not changed in more than twenty years. If you do not know both the unchanged foundation and the menu that Windows 11 split in two, you cannot isolate “the menu does not appear”, “it is hidden”, or “it appears twice”.
This article is aimed at IT staff at small and midsize companies and at Windows developers who look after business apps. It ties together, in a single picture, the three-layer structure of file association, caveats of classic shell extensions, how to target the Windows 11 new context menu, and installer registration, cleanup, and troubleshooting.
1. The Bottom Line First
- The foundation of the context menu and of file association is the three-layer registry structure “extension key → ProgID → verb”. The extension key is a pointer to a ProgID, the ProgID is the substance, and
shell\<verb>\commandunder it holds the command line.1 - HKEY_CLASSES_ROOT (HKCR) is not an independent hive; it is a merged view of HKLM\Software\Classes and HKCU\Software\Classes. Write all-user registration to HKLM and per-user registration to HKCU, and treat HKCR as read-only.2
- The default app (the app that opens on double-click) is designed to be chosen by the user, and a program cannot steal it. The OS protects the user’s choice; what an installer can do is register as a candidate.3
- A classic shell extension is an in-process COM DLL loaded into Explorer. A crash or delay in the extension spreads to Explorer as a whole (and to other apps that use the shell); a 64-bit environment requires a 64-bit DLL; and a managed-code implementation is unsupported.45
- On Windows 11 the context menu split in two. The only commands that appear on the new menu are ones registered with IExplorerCommand plus package identity; classic IContextMenu extensions are moved to the old menu under “Show more options” (Shift+F10).67
- The official route for putting a custom command on the new menu is to register a native DLL that implements IExplorerCommand in an MSIX manifest (desktop4:FileExplorerContextMenus). An app that cannot become MSIX can be given identity alone with a sparse package (MSIX with external location).78
- If all you want is “open with this app”, an association and a static verb are still enough. You do not need a shell-extension DLL, and Microsoft itself states plainly “choose the simplest method that meets the requirements (a static verb)”.9
- After registration or a change, notify with SHChangeNotify(SHCNE_ASSOCCHANGED); at uninstall, delete the ProgID but do not delete the extension key’s default value — that is the official guide. Shell integration includes the design of the cleanup.110
In one sentence: the world of associations and verbs is unchanged; only how the menu is shown split in two on Windows 11. Below we walk through this from the foundation up.
2. How File Association Works — The Three-Layer Structure Extension Key → ProgID → Verb
2.1. Reading the Three-Layer Structure from One Example
What happens when you double-click a file of a given extension is decided by three layers of registry keys.1
HKEY_CLASSES_ROOT
.kmrpt ← (1) Extension key
(Default) = KomuraSoft.Report.1 ← A pointer that only names the ProgID
OpenWithProgids
KomuraSoft.Report.1 ← A candidate under "Open with"
KomuraSoft.Report.1 ← (2) ProgID (the substance of the association)
(Default) = Komura Report document
DefaultIcon
(Default) = "C:\Program Files\KomuraSoft\Report.exe",0
shell ← (3) List of verbs
open
command
(Default) = "C:\Program Files\KomuraSoft\Report.exe" "%1"
- (1) The extension key (
.kmrpt) only points at a ProgID name as its default value. Writing a command here directly is a mistake. - (2) The ProgID (
KomuraSoft.Report.1) is the substance of the association; it holds the display name, the icon, and the verb list. - (3) A verb is an action such as “open” or “print”, and the default value of
shell\open\commandis the command line that is actually launched.
This separation is why you can point several extensions (.kmrpt and .kmrpt-file, for example) at the same ProgID, or swap the ProgID when you upgrade the app.
flowchart TB
accTitle: The three-layer structure of file association
accDescr: The extension key is a pointer whose default value names a ProgID; the ProgID is the substance that holds the display name, icon, and verb list; and the default value of command under the verb is the command line that is actually launched
ext["Extension key .kmrpt"] -->|names the ProgID as default| pid["ProgID KomuraSoft.Report.1"]
pid --> vb["verb (open and others under shell)"]
vb --> cmd["command default value"]
cmd --> exe["Report.exe is launched"]
pid -.-> attr["Also holds the display name and DefaultIcon"]
Figure 1: The extension key is a pointer, the ProgID is the substance, and the verb’s command is the command line that is actually launched.
2.2. HKCR Is a “Merged View” — Where You Write Changes the Meaning
The example above is shown under HKEY_CLASSES_ROOT (HKCR), but HKCR is not a physical storage location; it is a merged view of HKLM\Software\Classes and HKCU\Software\Classes. If the same key exists in both, the HKCU side wins.2
flowchart TB
accTitle: HKCR is a merged view
accDescr: HKCR is HKLM and HKCU Classes laid on top of each other; if the same key exists in both, HKCU wins; write registration to HKLM or HKCU explicitly and treat HKCR as read-only
hklm["HKLM\\Software\\Classes (all users)"] --> hkcr["HKCR (merged view)"]
hkcu["HKCU\\Software\\Classes (per user)"] --> hkcr
hkcu -.-> win["If the same key exists, HKCU wins"]
hkcr -.-> ro["Treat it as read-only (for confirmation)"]
Figure 2: HKCR is how HKLM and HKCU Classes look when laid together; always name one or the other as the write destination.
| Write destination | Meaning | Rights required |
|---|---|---|
HKLM\Software\Classes |
Registration common to all users | Administrator |
HKCU\Software\Classes |
Registration for that user only | None |
Writing to HKCR directly |
Dispatched depending on where the existing key already lives | Depends |
In practice, the safe split is to always write registration to either HKLM or HKCU explicitly, and treat HKCR as read-only (for confirmation). The relationship with WOW64 registry redirection is also worth sorting out. Association data directly under HKLM\Software\Classes such as extension keys and ProgIDs has been shared between the 32-bit and 64-bit registry views since Windows 7, so a 32-bit installer writing it does not escape to the Wow6432Node side. Some COM-registration subkeys such as Classes\CLSID, on the other hand, are redirected, and when you register a shell extension (in-process COM) the 32-bit / 64-bit write split matters. Details are in “Registry 32-bit/64-bit Redirection and Virtualization Pitfalls”.
2.3. Registration on the App Side — App Paths, Applications, RegisteredApplications
There are also three kinds of registration on the app side, pairing with the file side (extension and ProgID).11
- App Paths (
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths): Registration that letsShellExecuteExlaunch by executable file name alone. Microsoft recommends this because you do not have to pollute the PATH environment variable. - Applications (
HKCR\Applications\<app.exe>): Defines the default way to open when an arbitrary file is handed over under “Open with”, and the app’s display name (FriendlyAppName). - RegisteredApplications + Capabilities: Declares the extensions and MIME types the app can handle, and is the registration that makes it appear as a candidate on the Windows Default apps settings page.
Most consultations of “our app does not appear in the Default apps list” are cases where the ProgID was registered and this Capabilities registration was omitted.
flowchart TB
accTitle: The three kinds of registration on the app side
accDescr: App-side registration has three kinds — App Paths, Applications, and RegisteredApplications — responsible respectively for launch by file name alone, the default way to open under Open with, and appearance on the Default apps settings page
app["App-side registration"] --> ap["App Paths"]
app --> apps["Applications"]
app --> ra["RegisteredApplications"]
ap --> r1["Launch by file name alone"]
apps --> r2["Default under Open with"]
ra --> r3["Appears on the Default apps page"]
r3 -.-> cap["A Capabilities declaration is required"]
Figure 3: There are three kinds of app-side registration, and appearing as a Default apps candidate requires a Capabilities registration.
2.4. The Default App Belongs to the User — UserChoice Protection
Writing a ProgID as the extension key’s default value does not by itself make it the default app. The result of the user explicitly choosing under “Open with” and similar is kept in HKCU\...\Explorer\FileExts\<extension>\UserChoice, and association resolution prefers that side.
And the important point is that Windows does not support programmatic change of the default app. Default-app settings are designed to be done by the user through the system Settings UI; UserChoice data is obfuscated, and a filter driver (UCPD.sys) blocks writes from apps. In a managed environment, Group Policy / MDM policy is the official means.3
That tools such as SetUserFTA, which “imitate the hash and rewrite it”, have been used is the other side of this protection. What you should put in an in-house app’s installer is not stealing the default, but the three of (a) correct registration of the ProgID and verbs, (b) adding yourself to OpenWithProgIds, and (c) if needed, steering to the Settings page.
flowchart TB
accTitle: Default-app resolution and UserChoice protection
accDescr: The result of an explicit user choice is kept in UserChoice and preferred in association resolution; UCPD.sys blocks rewrites from apps, so what an installer can do is register as a candidate and steer to the Settings page
uc["UserChoice (the user's choice)"] -->|preferred| res["Association resolution"]
ext["Extension-key default value"] --> res
wr["Rewrite from an app"] -.->|UCPD.sys blocks it| uc
res ~~~ inst["The installer's job"]
inst --> a1["Register the ProgID and verbs"]
inst --> a2["Add to OpenWithProgIds"]
inst --> a3["Steer to the Settings page"]
Figure 4: Association resolution prefers the user’s choice (UserChoice), and the OS protects it from rewrites by apps.
3. Verbs Other Than “Open” — print, edit, runas, Custom Verbs
A verb is not only open. Standard verbs whose meaning the OS knows include edit, print, play, and preview as well as open, and a standard verb automatically gets a display name that follows the OS locale. The default verb used on double-click is decided in the order: the shell key’s default value → the first verb in the registry → open → openwith.12
flowchart TB
accTitle: Order in which the default verb is decided
accDescr: The default verb used on double-click is the first one found in the order shell-key default value, first verb in the registry, open, openwith
s1["shell-key default value"] -->|if none| s2["First verb in the registry"]
s2 -->|if none| s3["open"]
s3 -->|if none| s4["openwith"]
Figure 5: The default verb on double-click is the first one found in this order.
When you want to add your own action, register a custom verb.
KomuraSoft.Report.1
shell
open
command
(Default) = "C:\Program Files\KomuraSoft\Report.exe" "%1"
print
command
(Default) = "C:\Program Files\KomuraSoft\Report.exe" /print "%1"
verify ← custom verb
(Default) = Verify report (&V) ← menu display name
command
(Default) = "C:\Program Files\KomuraSoft\Report.exe" /verify "%1"
Three small facts that help to know.
- Register a verb named runas and you define an elevation launch equivalent to “Run as administrator”, and it is also used when a
ShellExecute-family API specifiesrunas. - Put an empty value named
Extendedon the verb key and it becomes an extended verb shown only when you Shift+right-click. Convenient for hiding a dangerous operation you rarely use.12 - Some associations of older apps still have a configuration that sends a document into an existing process with DDE (the
ddeexeckey), but launching a verb via DDE is already a Deprecated legacy. There is no reason to write it new.12
One more accident that happens often is quoting on the command line. If an element of the command string can contain a space, you must wrap it in quotes. That applies of course to an EXE path such as C:\Program Files\..., and %1 (the path of the selected file) should always be written "%1". You cannot guarantee that a user’s file path contains no space. An unquoted My Program.exe is interpreted as “launch My with the argument Program.exe”.13
flowchart TB
accTitle: The quoting accident on the command line
accDescr: An unquoted command is split at the space and misinterpreted as launching My with the argument Program.exe, so an EXE path that can contain a space and %1, which represents the selected file path, should always be wrapped in quotes
c1["Unquoted command"] -->|split at the space| bad["Misinterpreted as launching a different EXE"]
c2["Quoted command"] --> good["Launches as intended"]
c2 -.-> q1["Wrap the EXE path in quotes"]
q1 -.-> q2["Always wrap %1 in quotes too"]
Figure 6: An unquoted command is wrongly split at a space, so always wrap the EXE path and %1 in quotes.
The registry-only mechanism so far (a static verb) can be realized without writing a single DLL, and it does not risk making Explorer unstable. Microsoft itself repeatedly says “before you write a shell extension, consider whether the simplest static verb that meets the requirements will do”.9
4. Classic Shell Extensions — a DLL That Runs Inside Explorer
4.1. Kinds of Shell Extension
Requirements a static verb cannot meet — “change the menu dynamically depending on the selection”, “replace the icon or the property sheet” — use a shell-extension handler. Representative kinds are as follows.4
| Handler | Main interface | What it can do |
|---|---|---|
| Context-menu handler | IContextMenu + IShellExtInit | Dynamically add and control menu items |
| Icon handler / icon overlay | IExtractIcon / IShellIconOverlayIdentifier | Per-file icon and overlay |
| Property-sheet handler | IShellPropSheetExt | Add a tab to the property sheet |
| Thumbnail / infotip | IThumbnailProvider / IQueryInfo | Thumbnail view and hover description |
| Drag-and-drop / copy-hook handler | IDropTarget / ICopyHook | Intervene at drop or at copy/move |
These are all implemented as COM classes and registered in the registry by CLSID. The idea of COM itself is covered in “What Are COM / ActiveX / OCX?”.
4.2. What Being an In-Process COM Server Means
The essence of a classic shell extension is that it is an in-process COM server (DLL) loaded into Explorer (or into any app that opened a common file dialog). Every caveat follows from that.4
- If the extension crashes, Explorer is taken down with it. If it hangs, a right-click freezes for several seconds. The damage is also not limited to Explorer; it reaches every app that displayed a file-open dialog.
- Menu construction happens on the UI thread, so you must not do slow work such as network access or file I/O at menu-display time.
- Register the threading model as
Apartmentas a rule.
flowchart TB
accTitle: Collateral-damage structure of an in-process extension
accDescr: A shell-extension DLL is loaded not only into Explorer but also into the process of any app that opened a file dialog, so a crash or hang in the extension spreads to the whole host process
dll["Shell-extension DLL"] -->|loaded in-process| exp["Explorer"]
dll -->|loaded in-process| any["Any app that opens a dialog"]
exp --> dmg["A crash or hang spreads"]
any --> dmg
dmg -.-> rule["Do not do slow work at display time"]
Figure 7: The extension DLL runs inside the host process, so a crash or hang spreads to the host as a whole.
Investigate a consultation such as “Explorer freezes when I open a particular folder” or “a right-click takes five seconds” and it is not rare for the cause to be a third-party shell extension rather than the in-house app. Isolation methods are in Chapter 8.
4.3. Matching Bitness — a 64-bit Environment Requires a 64-bit DLL
An in-process DLL must match the bitness of the process that loads it. Explorer on 64-bit Windows is a 64-bit process, so a shell-extension DLL built only as 32-bit is never loaded and never appears on the menu at all. There is also no error, so it is a staple cause of “I registered it but it does not appear”. Combining a 32-bit app body with a 64-bit shell-extension DLL is a legitimate configuration, but you need to watch the fact that COM registration splits by bitness (Wow6432Node). Launching from a verb’s command is a separate-process EXE, so it is not subject to this constraint (leaving it a 32-bit EXE is fine).
flowchart TB
accTitle: Matching bitness of a shell-extension DLL
accDescr: The only shell-extension DLL a 64-bit Explorer can load is a 64-bit one; a 32-bit-only DLL never appears on the menu and produces no error; an EXE launched from a verb command is a separate process and is not subject to the constraint
exp["64-bit Explorer"] -->|can load| d64["64-bit shell-extension DLL"]
exp -.->|cannot load| d32["32-bit-only DLL"]
d32 -.-> sym["Does not appear on the menu, with no error"]
exe["EXE launched from a verb"] -->|separate process| ok32["Fine left as 32-bit"]
Figure 8: The only DLL loaded into 64-bit Explorer is a 64-bit DLL; an EXE launched from a verb is not subject to this constraint.
4.4. Why You Must Not Write It in Managed Code
I often get the question “can I write a shell extension in C#”, but Microsoft has stated plainly that writing an in-process shell extension in managed code (.NET) is not recommended and is out of support.5
The reason is the nature of the extension being loaded into an arbitrary process. CLR version collisions (especially below .NET Framework 4), the problem of the CLR reentering the message loop while waiting on a lock, and non-deterministic object lifetime from garbage collection colliding with COM’s reference-count contract are structural reasons the host app becomes unstable. Some items have been mitigated on .NET Framework 4 and later and on modern .NET, but the official position has not changed.
The practical guideline is simple. Write an in-process extension in native C++. If you want to use managed code, make it a normal EXE launched from a verb’s command, or an out-of-process extension that runs in a separate process (a preview handler and similar).5
flowchart TB
accTitle: Judging whether managed code is allowed
accDescr: An in-process extension that runs inside Explorer is written in native C++ as a rule; if you want managed code, make it a normal EXE launched from a verb command or an out-of-process extension that runs in a separate process
q1{"Runs in-process?"} -->|yes| cpp["Write it in native C++"]
q1 -->|no| mg["Managed code is fine"]
cpp -.-> why["CLR / reentrancy risk"]
why -.-> whyN["Host becomes unstable"]
mg --> e1["Verb-launched EXE"]
mg --> e2["Out-of-process preview"]
Figure 9: An in-process extension is native C++ as a rule; managed code is limited to a configuration that runs in a separate process.
5. The Windows 11 New Context Menu — the Menu Split in Two
5.1. What Happened
Windows 11 refreshed File Explorer’s context menu. Cut, copy, and similar became a row of icons at the top; “Open” and “Open with” were grouped together at the top; and commands an app adds are grouped below the shell’s standard commands. When one app adds several commands, they are gathered into a flyout (submenu) named after the app.6
And the crucial point is this. Classic IContextMenu-based shell extensions were not deleted; they were moved to the old-menu side that opens with “Show more options” (Shift+F10) and loads the Windows 10 menu as-is.6 The identity of the opening consultation’s “the menu was hidden” is this split.
flowchart TB
accTitle: The context menu Windows 11 split in two
accDescr: What opens first on a right-click is the new menu; the only commands that appear there are ones registered with IExplorerCommand and package identity; classic IContextMenu extensions are moved to the old menu that opens with Show more options
rc["Right-click a file"] --> newm["New menu (Windows 11)"]
newm --> newi["IExplorerCommand + identity commands"]
newm -->|Show more options Shift+F10| oldm["Old menu (the Windows 10 menu)"]
oldm --> oldi["Classic IContextMenu extensions"]
newi -.-> fly["Several commands are gathered into a flyout"]
Figure 10: The only commands that appear on the new menu are IExplorerCommand + identity commands; classic extensions are moved to the old-menu side.
5.2. The Official Route onto the New Menu — IExplorerCommand + Manifest Registration
There is one way to put a custom command on the new menu. Prepare a native DLL that implements the IExplorerCommand interface, and declare the COM server and the context-menu extension in an MSIX package manifest.7
<!-- Package manifest (excerpt) -->
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:SurrogateServer DisplayName="Komura commands">
<com:Class Id="01234567-89AB-CDEF-0123-456789ABCDEF"
Path="KomuraCommand.dll" ThreadingModel="STA" />
</com:SurrogateServer>
</com:ComServer>
</com:Extension>
<desktop4:Extension Category="windows.fileExplorerContextMenus">
<desktop4:FileExplorerContextMenus>
<desktop5:ItemType Type=".kmrpt">
<desktop5:Verb Id="VerifyReport"
Clsid="01234567-89AB-CDEF-0123-456789ABCDEF" />
</desktop5:ItemType>
</desktop4:FileExplorerContextMenus>
</desktop4:Extension>
ItemType’s Type can specify a particular extension, or * (all files), Directory (folders), or Directory\Background (a folder background). Match the DLL to Explorer’s architecture (64-bit / ARM64).7
IExplorerCommand itself is an interface that has existed since the Windows 7 era; you implement the title (GetTitle), the icon (GetIcon), enabled / disabled / hidden state (GetState), and execution (Invoke). The methods are called from the UI thread, so access to network resources is forbidden, and menu-construction methods need to return quickly. Do heavy work after Invoke.147
flowchart TB
accTitle: Manifest structure of a new-menu registration
accDescr: The MSIX manifest's COM-server declaration maps a CLSID to a DLL, and the context-menu-extension declaration ties the target and the implementation together with ItemType and Verb, so a custom command appears on the new menu
man["MSIX manifest"] --> com["COM-server declaration"]
man --> ctx["Menu-extension declaration"]
com -->|maps CLSID to DLL| impl["IExplorerCommand implementation DLL"]
ctx -->|specifies with ItemType and Verb| impl
impl --> shown["Command appears on the new menu"]
ctx -.-> tgt["The target is an extension, all files, and similar"]
Figure 11: Two declarations in the manifest tie the implementation DLL to the target, and the command appears on the new menu.
5.3. The Option for a Non-Packaged App — Getting Identity Alone with a Sparse Package
The escape hatch when “our app cannot be distributed except as MSI; MSIX is impossible” is a sparse package (MSIX with external location). You sign a small MSIX that is only a manifest, containing no app body, and register it at the end of the existing installer. The app then acquires package identity, and the manifest registration above (= appearance on the new menu) becomes possible. It is available from Windows 10 version 2004 onward, and the package needs a signature with a certificate trusted on the target machine.8
flowchart TB
accTitle: The flow of obtaining identity with a sparse package
accDescr: After the existing installer places the app body, registering a manifest-only sparse package with an external location gives the app package identity and makes new-menu manifest registration possible
inst["Existing installer"] --> files["Place the app body"]
sp["Sparse package"] -.-> only["Manifest only, no body"]
files --> reg["Register with an external location"]
sp --> reg
reg --> id["Acquire package identity"]
id --> ok["New-menu registration becomes possible"]
sp -.-> sign["A trusted signature is required"]
Figure 12: Register a sparse package that contains no body, with an external location, and the app acquires package identity.
The greatest advantage is that you do not have to replace the installer; it is the realistic answer for an app that already has an MSI/EXE installer asset. For a comparison with a full move to MSIX, see also “Choosing a Windows App Distribution Method”.
5.4. How Association Verbs Appear on the New Menu
A point that is easy to misunderstand: the associations of Chapters 2 and 3 (ProgID and verb) are still alive on the new menu. The default verb on double-click, “Open”, and the “Open with” candidates are resolved from the association and displayed at the top of the new menu. So if all you want is “to be able to open with this app”, Windows 11 needs no extra work. On the other hand, an association is not a general-purpose menu extension, so if you want an arbitrary custom command on the first layer of the new menu you need IExplorerCommand plus identity — that is the split of roles.7
flowchart TB
accTitle: The split of roles between associations and the new menu
accDescr: A ProgID-and-verb association is still used on the new menu to resolve the default verb, Open, and Open with, and is displayed at the top; putting an arbitrary custom command on the first layer of the new menu requires IExplorerCommand and identity
assoc["Assoc.(ProgID + verb)"] --> sol["Resolve default / Open"]
sol --> top["Top of the new menu"]
assoc -.-> keep["No extra work on Win11"]
cmd["A custom command"] --> need["IExplorerCommand"]
need -.-> needN["plus identity"]
need --> first["New-menu first layer"]
Figure 13: Associations still handle “Open”-family resolution on the new menu; only a custom command requires IExplorerCommand plus identity.
6. A Practical Decision Table — Which of the Three Options to Take
We organize what we have so far into a practical three-way choice.
| What you want to achieve | Recommended means | How it looks on Windows 11 | Work and cost required |
|---|---|---|---|
| (a) Launch the in-house app on double-click or “Open” | Association + static verb (registry registration only) | Integrated into “Open” and “Open with” on the new menu | Installer registry registration only. No DLL, no extra signing requirement |
| (b) Put a custom command for the selected file/folder on the new menu | IExplorerCommand implementation + MSIX manifest registration. A non-packaged app is given identity with a sparse package | First layer of the new menu (several commands are gathered into an app-name flyout) | Native C++ DLL + package identity + code signing |
| (c) Keep using an existing classic IContextMenu extension | Keep it as-is for now (do not choose it for new development) | Old-menu side only, under “Show more options” (Shift+F10) | Maintain a 64-bit build and COM registration. Plan a later move to (b) |
There are two judgement points. First, do not bring in (b) or (c) for a requirement that (a) will meet. The moment you write a shell extension you take on responsibility for Explorer’s stability. Second, (c) is only “not broken”; as a user experience it stays one step worse. The more frequently a command is used in daily operation, the larger the return on a move to (b).
flowchart TB
accTitle: How to choose among the three options
accDescr: If all you want is launch on double-click or Open, an association and a static verb are enough; to put a custom command on the new menu use IExplorerCommand and MSIX manifest registration; if you cannot become MSIX, grant identity with a sparse package; keep an existing classic IContextMenu extension on the old-menu side for now
q1{"Is Open enough?"} -->|yes| pa["Assoc. + static verb"]
q1 -->|no| q2{"Custom on new menu?"}
q2 -->|yes| q3{"Can you become MSIX?"}
q3 -->|yes| pb1["IExplorerCommand+MSIX"]
q3 -->|no| pb2["Sparse-pkg identity"]
q2 -->|no| pc["Keep classic for now"]
pc -.-> old["Old-menu side only"]
pa -.-> dllfree["No DLL, small risk"]
Figure 14: Choose among a static verb, IExplorerCommand plus identity, and keeping the classic one, according to the requirement.
7. Deployment and Registration in Practice — Installer, Sparse Package, Cleanup
7.1. HKLM or HKCU
Match it to the shape of the installer. All-user (placed under Program Files, administrator rights) is HKLM\Software\Classes; per-user install (no elevation) is HKCU\Software\Classes. Mix them and you produce the “A can open it but B cannot” kind of inquiry. For a shell extension that involves CLSID registration, Reg-Free COM — which removes the need for registry registration itself — is a valid option for in-app COM use, but it cannot be applied to a shell extension Explorer loads, so you need the straightforward registration (“What Is Reg-Free COM”).
7.2. After a Change, Notify — SHChangeNotify
After you register, change, or delete an association, notify the SHCNE_ASSOCCHANGED event with SHChangeNotify. Skip this and Explorer can fail to notice the change until a reboot.110
// Call once after changing associations, e.g. from an installer custom action
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr);
7.3. Registering and Removing a Sparse Package
Registering and removing a sparse package is the installer’s job. Register after placing the files; remove before deleting the files.8
# At install time: after placing the files, register the install folder as the external location
Add-AppxPackage -Path "C:\Program Files\KomuraSoft\KomuraReport.identity.msix" `
-ExternalLocation "C:\Program Files\KomuraSoft"
# At uninstall time: remove the package registration before deleting the files
Remove-AppxPackage <package full name>
A point to watch: Add-AppxPackage registers for the user who ran it. If you call it from a custom action of a per-machine MSI, running under LocalSystem does not grant identity to the user who installed, so you configure it to run under user impersonation. Even then, the registration under impersonation is only for the user who ran that install. On a PC used by several users, other users and users created later have no package identity, and the command does not appear on the new menu. To have every user use it, provide a mechanism such as checking your own package registration at first launch and registering if it is missing (per-user registration), and include removal from each user who has a registration in the uninstall plan. Reflecting a manifest registration can also require an Explorer restart (or a sign-out).7
flowchart TB
accTitle: Order of registering and removing a sparse package
accDescr: At install time register the sparse package after placing the files; at uninstall time remove the registration before deleting the files; watch the fact that registration is effective only for the user who ran it
i1["Install"] --> i2["Place the files"]
i2 --> i3["Register the sparse package"]
u1["Uninstall"] --> u2["Remove the package registration"]
u2 --> u3["Delete the files"]
i3 -.-> pu["Registration is effective only for the running user"]
Figure 15: Register after placing the files, remove before deleting the files, and watch the fact that registration is per running user.
7.4. Cleanup at Uninstall — What to Delete and What to Leave
Cleanup at uninstall has a clear official-guide line.1
- Delete: The whole in-house ProgID key, Capabilities/RegisteredApplications registration, the shell extension’s CLSID registration, the sparse package (Remove-AppxPackage).
- Leave: The extension key (
.kmrpt) default value. The official recommendation is not to delete it even if it still points at the in-house ProgID. Judging after install whether another app has taken the default is difficult, and Windows simply ignores a default-value ProgID that is unregistered, so leaving it does no real harm. - Call SHChangeNotify(SHCNE_ASSOCCHANGED) at the end of the cleanup as well.
Most “we uninstalled and leftovers still appear on the menu” problems are a leak in this cleanup design.
flowchart TB
accTitle: Cleanup design at uninstall
accDescr: At uninstall you delete the in-house ProgID key, CLSID registration, and sparse package; leave the extension-key default value because an unregistered ProgID is ignored; notify the change with SHChangeNotify at the end of the cleanup
un["Uninstall"] --> del["Delete"]
un --> keep["Leave"]
del --> d1["ProgID and CLSID registration"]
del --> d2["Sparse package"]
keep --> k1["Extension-key default value"]
k1 -.-> why["An unregistered ProgID is ignored"]
d1 --> fin["Notify with SHChangeNotify at the end"]
k1 --> fin
Figure 16: Delete the in-house registration, leave the extension-key default value, and notify the change at the end of the cleanup.
8. Troubleshooting — Missing, Duplicate, Heavy
8.1. It Does Not Appear on the Menu
Isolate in this order.
- Which menu you are looking at: A classic-style registration only appears on the old-menu side under Shift+F10. Check both first.
- Bitness: A 32-bit-only shell-extension DLL is not loaded into 64-bit Explorer (Section 4.3).
- Registration destination: HKLM/HKCU, a Wow6432Node mix-up. Confirm the actual key with
reg query. - Package registration: For the new menu, confirm presence with
Get-AppxPackage, trust of the signing certificate, and the-ExternalLocationpath, then restart Explorer.7 - Missed notification: If SHChangeNotify was forgotten, you can tell by whether an Explorer restart makes it take effect.
flowchart TB
accTitle: Isolation order when it does not appear on the menu
accDescr: Start by confirming which menu you are looking at, then isolate DLL bitness, registry registration destination, package registration and signing, and a missed SHChangeNotify, in that order
c1["Confirm which menu, old or new"] --> c2["Confirm DLL bitness"]
c2 --> c3["Confirm HKLM and HKCU registration destination"]
c3 --> c4["Confirm package registration and signing"]
c4 --> c5["Tell a missed notification by restarting"]
Figure 17: When it “does not appear”, isolate in the order menu you are looking at, bitness, registration destination, package registration, missed notification.
8.2. It Appears Twice, or Will Not Go Away
Typical causes are coexistence of a classic registry registration and a manifest registration, a leak in uninstall cleanup (Section 7.4), or leftovers of an old-version ProgID. If it appears twice only on the old menu, think leftovers; if it appears on both old and new, think coexistence.
flowchart TB
accTitle: Isolating a double display
accDescr: Twice only on the old menu points at leftovers such as a cleanup leak or an old ProgID; twice on both old and new points at coexistence of a classic registry registration and a manifest registration
q{"On which does it appear twice?"} -->|old menu only| zan["Leftovers"]
q -->|both old and new| hei["Coexistence"]
zan -.-> z1["Cleanup leak or an old ProgID left behind"]
hei -.-> h1["Classic registry registration coexisting with the new registration"]
Figure 18: Twice only on the old menu points at leftovers; twice on both old and new points at coexistence.
8.3. Explorer Is Heavy or Crashes
When a right-click is slow, or a particular folder crashes, first inventory the installed shell extensions. List non-Microsoft extensions with a tool such as NirSoft’s ShellExView, temporarily disable the suspicious ones, and binary-search to identify the culprit DLL. On a crash, Event Viewer’s “Faulting module” is also a clue. If the in-house extension was the cause, suspect synchronous I/O or network access on the menu-construction path (Sections 4.2 and 5.2).
flowchart TB
accTitle: Identifying the culprit DLL when it is heavy or crashes
accDescr: List non-Microsoft shell extensions in ShellExView, temporarily disable the suspicious ones and binary-search to identify the culprit DLL; on a crash, Event Viewer's faulting module is also a clue
s1["Inventory shell extensions"] --> s2["List the non-Microsoft ones"]
s2 --> s3["Temporarily disable and binary-search"]
s3 --> s4["Identify the culprit DLL"]
crash["On a crash"] -.-> ev["Check the faulting module"]
ev -.-> s4
Figure 19: Temporarily disable non-Microsoft extensions and binary-search; on a crash, also use Event Viewer.
8.4. Windows Sandbox Is Convenient for Verification
Verification of shell integration is based on confirming “install on a clean environment → operate → uninstall → leftovers zero”. Convenient here is Windows Sandbox (Pro/Enterprise/Education): every launch brings up a brand-new disposable Windows in a few seconds, so you can run installer registration and cleanup tests as many times as you like. Close it and everything disappears, so it also suits leftover-registry investigation.15
9. Summary
- File association is the three-layer structure “extension key → ProgID → verb”, and HKCR is a merged view of HKLM/HKCU Classes. Name the write destination explicitly, and always wrap
%1in quotes. - The default app is designed to be chosen by the user and cannot be changed from a program. The installer’s job is to register correctly as a candidate.
- A classic shell extension is an in-process COM DLL loaded into Explorer. A crash or delay spreads to the whole; 64-bit is required; managed code is unsupported; implementation in native C++ is the rule.
- On Windows 11 the context menu split in two. Putting a custom command on the new menu requires IExplorerCommand plus an MSIX manifest; classic IContextMenu is moved to the “Show more options” side.
- For an app that cannot become MSIX, obtaining identity with a sparse package (MSIX with external location) is the realistic answer.
- If all you want is “open with this app”, an association and a static verb are still enough. Starting from the simplest means is the official guideline as well.
- After registration, a change, or a deletion, notify with SHChangeNotify; at uninstall, delete the ProgID but leave the extension-key default value. Windows Sandbox is convenient for verification.
If a Windows 11 PC replacement made you notice that “the menu was hidden”, first confirm which of (a), (b), and (c) in Chapter 6’s decision table it is. You should be able to estimate the scale of the work on the spot.
Related Articles
- What Are COM / ActiveX / OCX? - The Differences and Relationships Explained
- What Is Reg-Free COM - Using COM Without Registration
- Registry 32-bit/64-bit Redirection and Virtualization Pitfalls — Wow6432Node and the “The Value I Wrote Isn’t There” Problem
- Choosing a Windows App Distribution Method - MSI/MSIX/ClickOnce/xcopy/Custom Updater
- DLL and COM Interface Backward Compatibility — A Decision Table for Which Changes Break Callers
- How Windows App Compatibility Works — Keeping Old Apps Alive with Compatibility Mode, Shims, and Compatibility Administrator
Related Consulting Areas
KomuraSoft LLC handles the design and implementation of file association, context menus, and shell extensions for business apps; targeting the Windows 11 new context menu (moving to IExplorerCommand, introducing a sparse package); reviewing registration and cleanup of an existing installer; and investigating the cause of Explorer being heavy or crashing. It is fine to start from deciding what to do about “the menu that was hidden under Show more options”.
- Windows Application Development
- Legacy Asset Migration
- Technical Consulting & Design Review
- Contact Us
References
-
Microsoft Learn, File Types. On the structure of an extension key pointing at a ProgID; OpenWithProgIds; the split of registration between HKLM/HKCU\Software\Classes; calling SHChangeNotify(SHCNE_ASSOCCHANGED) after an association change; and deleting the ProgID at uninstall while leaving the extension key’s default value. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, HKEY_CLASSES_ROOT Key. On HKEY_CLASSES_ROOT being a merged view of HKLM\Software\Classes and HKCU\Software\Classes; user-side definitions taking priority over machine-side ones; and the dispatch rules when writing. ↩ ↩2
-
Microsoft Learn, Windows app defaults platform. On changing the default app being designed to be done only through the system Settings UI; user-settings data being obfuscated and write-protected by a filter driver (UCPD.sys); registry-based change being unsupported; and using Group Policy / MDM policy in a managed environment. ↩ ↩2
-
Microsoft Learn, Working with Shell Extensions. On the kinds of shell-extension handler; an extension being an in-process COM DLL loaded into Explorer (and into processes that host the shell), so that a crash or hang spreads to Explorer as a whole; registration with ThreadingModel=Apartment; and considering a simpler alternative before a shell extension. ↩ ↩2 ↩3
-
Microsoft Learn, Guidance for Implementing In-Process Extensions. On Microsoft not recommending and not supporting an in-process shell-extension implementation in managed code; reasons including CLR version collisions, reentrancy, and non-deterministic object lifetime; and managed code being acceptable for an out-of-process extension (a preview handler, or a launch from shell\verb\command). ↩ ↩2 ↩3
-
Windows Developer Blog, Extending the Context Menu and Share Dialog in Windows 11. On the design of the Windows 11 new context menu; extension via IExplorerCommand plus app identity; placing “Open” and “Open with” at the top; gathering several commands into an app-name flyout; and classic IContextMenu extensions being loaded as the Windows 10 menu under “Show more options” (Shift+F10). ↩ ↩2 ↩3
-
Microsoft Learn, Add a File Explorer context menu command to a packaged desktop app. On registration on the Windows 11 new context menu being done with an IExplorerCommand implementation plus windows.comServer plus a desktop4:FileExplorerContextMenus manifest declaration; ItemType being able to specify *, Directory, or Directory\Background; matching DLL architecture; keeping menu-construction methods fast; covering a non-packaged app with a sparse package; an Explorer restart sometimes being required for the registration to take effect; and a file association not being a general-purpose menu extension. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Microsoft Learn, Grant package identity by packaging with external location. On obtaining package identity by registering an external-location package (sparse package) without changing the existing installer; availability from Windows 10 version 2004 onward; and Windows features that require identity (context-menu registration, notifications, and similar) becoming usable. ↩ ↩2 ↩3
-
Microsoft Learn, Choosing a Static or Dynamic Shortcut Menu Method. On choosing the simplest static-verb method that meets the requirements; IContextMenu being the most powerful but also the most complex and classified toward the not-recommended side; and IExplorerCommand/IExplorerCommandState being the recommended method. ↩ ↩2
-
Microsoft Learn, SHChangeNotify function. On how to raise the SHCNE_ASSOCCHANGED event that notifies the system of a file-association change, and on using it so the shell notices the change. ↩ ↩2
-
Microsoft Learn, Application Registration. On registration of an executable via the App Paths subkey being recommended; the role of the Applications subkey; verb registration via SystemFileAssociations; and the priority of the ProgID and related information when the default app changes. ↩
-
Microsoft Learn, Verbs and File Associations. On a verb being an action also used by ShellExecuteEx; elements of a command string that can contain a space needing to be wrapped in quotes, and “%1” always being written quoted; and registering a default procedure under HKCR\Applications. ↩
-
Microsoft Learn, IExplorerCommand interface. On the method makeup of GetTitle, GetIcon, GetState, Invoke, EnumSubCommands, and similar; methods being called on the UI thread so they must not communicate with network resources; and availability from Windows Vista onward. ↩
-
Microsoft Learn, Windows Sandbox. On being able to launch a disposable, isolated Windows environment in a few seconds, all changes being discarded when you close it, it being suitable for software testing and installer verification, and availability on Pro/Enterprise/Education. ↩
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
How the Clipboard and Drag & Drop Work — Handling OLE Data Transfer Correctly in Business Apps
Paste an Excel table and the formatting falls apart; close the source app and you can no longer paste — both come from the clipboard plac...
Windows App Outsourcing and Contract Development: What to Sort Out Before You Ask
Before commissioning Windows app outsourcing or contract development, here is how to sort out existing software modification, device inte...
A Developer's Strange Love, or: How I Learned to Stop Worrying and Love Windows
Windows is a hassle. But that hassle is the hassle of an OS that has carried real-world business on its back.
Why Windows Shows "Windows protected your PC"
Why your app triggers "Windows protected your PC": SmartScreen checks publisher reputation, not just signatures. Compare OV/EV certs, MSI...
The Win32 Thread Pool API — Concurrency Without Creating Threads, via CreateThreadpoolWork
Are you spawning CreateThread calls all over your native code? This article explains the Win32 thread pool API redesigned in Vista — the ...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
ActiveX Migration
Topic page for staged decisions around keeping, wrapping, or replacing COM / ActiveX / OCX assets.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
We support Windows desktop applications that involve resident processing, device integration, operational logging, and maintainable structure.
Windows Software Maintenance & Modernization
We support staged upgrades, feature additions, 64-bit readiness, and maintainable restructuring for existing Windows software.
Frequently Asked Questions
Common questions about the topic of this article.
- Why does our app's context-menu item on Windows 11 only appear under "Show more options"?
- Because on Windows 11 File Explorer's context menu split into two layers, old and new. The only commands that can appear on the new menu are ones that implement the IExplorerCommand interface and are registered in an MSIX package manifest (= they have package identity). Classic IContextMenu-based shell extensions were moved to the old menu that you open with "Show more options" (Shift+F10). The extension itself is not broken, so it continues to work for now, but if you want it on the new menu you need a migration to IExplorerCommand and either packaging as MSIX or granting identity with a sparse package.
- Can the installer set our app as the default app for a file (the app that opens on double-click)?
- No. Choosing the default app is designed to be something the user does, and Windows does not support changing the default app from anywhere other than the system Settings UI. The UserChoice information that holds each user's choice is obfuscated, and a filter driver (UCPD.sys) also write-protects it from apps. What an installer can do is register a ProgID and verbs, add itself to OpenWithProgIds so it appears as a candidate under "Open with", and steer the user to the Default apps settings page. The correct implementation is not to steal the default, but to get ready to be chosen.
- May I write a shell extension in managed code such as C#?
- Microsoft has stated plainly that writing an in-process shell extension (a context-menu handler, an icon handler, and similar) in managed code is not recommended and is unsupported. The extension is loaded into Explorer and into the process of any app that opens a common file dialog, so CLR version collisions, reentrancy, and non-deterministic object lifetime make the host app unstable. Implementation in native C++ is the rule. A normal EXE launched from a verb's command, or an out-of-process extension such as a preview handler that runs in a separate process, is fine in managed code.
- What is a sparse package (MSIX with external location)?
- A small MSIX package that contains no app files, only a manifest (identity information). For an app installed the ordinary way with an existing installer (MSI, Inno Setup, and similar), you register it with Add-AppxPackage -ExternalLocation pointing at the install folder, and the app acquires package identity and can use features that require identity, such as registration on the Windows 11 new context menu and toast notifications. It is available from Windows 10 version 2004 onward, and the package needs a code signature trusted on the target machine. It is the realistic option when you want new-menu support without moving the whole distribution method to MSIX.
- What should I do when a context-menu item appears twice, or will not go away?
- First isolate the cause by checking which menu it appears on: the new menu or the old menu (Show more options). The typical double display is that a classic registry registration and an MSIX manifest registration coexist, or that a ProgID or extension CLSID registration was left behind at uninstall. After changing associations, also suspect a missed SHChangeNotify(SHCNE_ASSOCCHANGED); right after package registration, suspect a missed Explorer restart. If that still does not resolve it, temporarily disable non-Microsoft extensions in ShellExView and binary-search to identify the culprit DLL.