DEV Community

Cover image for A Windows context menu editor in one PowerShell file
ROCI
ROCI

Posted on Originally published at roee.ilouz.xyz

A Windows context menu editor in one PowerShell file

The Windows right-click menu is built from several different places, and Windows 11 hides half of it behind "Show more options". Most tools I found each cover one part of it. I wanted one list for all of it, so I built Context Menu Editor: a single PowerShell script with a WPF window, free and MIT licensed.

You can try it without installing anything:

irm https://rocisapps.com/cme | iex
Enter fullscreen mode Exit fullscreen mode

You can read the source first, and the same script is attached to every GitHub release.

Windows 11 shows a short right-click menu with

Context Menu Editor's Tweaks page: a checklist of menu clean-ups with Minimal and Standard presets and a Run tweaks button.

Here is how it works, and the parts that were harder than I expected.

Three kinds of entries

Windows builds the menu from three different places, and each one is switched off differently:

  • Shell verbs under Software\Classes\<class>\shell\<verb>. Adding a LegacyDisable value turns one off without deleting it.
  • Shell extension handlers under shellex\ContextMenuHandlers (7-Zip, antivirus, PowerToys). You turn one off by adding its CLSID to the per-user Shell Extensions\Blocked list.
  • Windows 11 packaged items, which apps declare in their AppxManifest.xml as windows.fileExplorerContextMenus. They have no command line at all, only a COM handler, and they are blocked the same way as extensions.

Reading all three into one list, with the right "off" mechanism behind each switch, is most of the work.

Diagram: three sources of right-click entries (shell verbs, shell extensions, Windows 11 packaged items), each turned off differently, feeding one list in Context Menu Editor.

# Turn a shell extension off for the current user (restart Explorer afterwards)
$blocked = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey(
    'Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Blocked')
$blocked.SetValue('{00000000-0000-0000-0000-000000000000}', '')   # the handler's CLSID
Enter fullscreen mode Exit fullscreen mode

Do not use the registry provider

All registry access goes through Microsoft.Win32.Registry. The PowerShell registry provider treats the * in HKCU\Software\Classes\*\shell as a wildcard, so a path that means "all files" quietly matches other keys. The .NET API takes the path literally:

$shell = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Software\Classes\*\shell')
$shell.GetSubKeyNames()
Enter fullscreen mode Exit fullscreen mode

Send to, and items with no command line

Send to is a folder of shortcuts, so adding an app is easy. Windows 11 packaged items are not: Blip, for example, has a submenu of devices and no executable to point a shortcut at. The shortcut targets a small C# helper instead, which finds the item by its CLSID and calls it on the selected files, the way Explorer does. Submenu entries are matched by title first and by position only as a fallback, so pairing a new device does not change where an existing shortcut sends.

The helper is compiled at runtime with Add-Type and stored outside the script's folder, so shortcuts keep working if the editor moves.

Diagram: a Send to shortcut calls a small C# helper, which calls the Explorer command by CLSID, which runs the app's menu item.

One script, built from many files

The source is split into small files: core functions, UI, XAML, and JSON for the tweaks list. A Compile.ps1 stitches them into the single script people run. Two details only showed up with the one-line launch:

  • irm | iex passes a byte order mark through as code, which breaks parsing. The output is therefore pure ASCII and written without a BOM, and the compiler refuses non-ASCII characters.
  • Windows PowerShell 5.1 reads a BOM-less file as ANSI, which is only safe because of the ASCII rule.

The short URL is a redirect on my own domain to the latest GitHub release asset, so a new release is live as soon as it is published.

Startup time

The first version took about two seconds before any window appeared, and most of that was reading the registry. Two changes helped.

Open the window first. It appears straight away with a "reading" state, and the scan runs once the first frame is on screen:

$Window.Add_ContentRendered({
    $Window.Dispatcher.BeginInvoke(
        [System.Windows.Threading.DispatcherPriority]::Background,
        [Action]{ Invoke-Scan; Update-View })
})
Enter fullscreen mode Exit fullscreen mode

The window now appears in 0.6 to 0.8 seconds, and the full list is ready at about the same total time as before.

Bar chart: time until the window appears dropped from 1.9 to 0.6 seconds on Windows PowerShell 5.1 and from 2.4 to 0.8 seconds on PowerShell 7.

Cache the compiled helper. Compiling the C# types cost 0.1 to 0.3 seconds on every start. Now they are compiled once and loaded from a cache file in about 15 milliseconds:

Add-Type -TypeDefinition $source -OutputAssembly $dll -OutputType Library   # first run only
Add-Type -LiteralPath $dll                                                  # every run
Enter fullscreen mode Exit fullscreen mode

An elevated run never loads that cache. An administrator process should not load code from a folder a standard user can write to, so it compiles in memory instead.

PowerShell traps I hit

  • Variable names are case-insensitive, so a local $s quietly replaced a global $S. The shared state is now $App.
  • A function that returns an array with return ,$array can come out nested after @() or a pipeline. Returning the array directly is safer.
  • R is an alias for Invoke-History, which matters when a script defines a short helper function.
  • The `u{...} Unicode escape does not exist in Windows PowerShell 5.1.

Testing without touching your own menus

-LibraryOnly loads every function without opening the window, so the scanner and every write operation can be scripted. An early test run wrote into my real Send to folder, which taught me to point tests at a temporary Send to folder and a temporary backup folder. A separate sweep opens every page of the window and fails on any error, in both PowerShell 7 and 5.1.

Safety

Anything you edit or delete is exported to a .reg file first, and the last change can be undone from the status bar. Per-user changes need no administrator rights. The script makes no network calls except downloading itself again when you choose "run as administrator".

Try it, and tell me what is missing

I am the author. If something breaks on your setup, or an entry is missing from the list, open an issue on GitHub. Please include your Windows version, PowerShell edition and what caused it to break.

Top comments (0)