A configurable Windows Explorer context-menu extension derived from Microsoft's C++ Shell extension sample. Menu items, Shell registration targets, filters, gates, executors, and icons are configurable; complex behavior stays in C++.
This project was developed primarily with the assistance of AI Agents. Parts of the code, design, and documentation have not undergone sufficient, rigorous, or comprehensive testing. The project is provided for learning, research, and technical reference only; correctness, stability, and security are not guaranteed for all systems, environments, or use cases.
Before using this project in an important project, commercial software, or production environment, perform appropriate code review, customization, security assessment, and compatibility testing. The project author assumes no responsibility for data loss, system failures, business interruptions, or any other direct or indirect losses resulting from the use of this project.
- Registers five Shell target types by default: file, directory, directory background, drive, and file-system object.
- Configures menu items, target filters, selection filters, gate/executor chains, and actions in
config/menu.json. - Configures COM/Shell registration targets in
config/registration.json. - Validates the whole configuration strictly: wrong field types, unknown gates/executors, unknown
actionType, and duplicate ids fall back to the built-in menu instead of silently widening filters. - Validates
registration.jsonbefore touching the registry; an invalid file leaves an existing installation unchanged. Successful register/unregister calls notify the Shell. - Supports
messageBoxandlaunchactions plus custom executors. - Supports SVG/BMP icons; SVG files are loaded and rasterized by the existing NanoSVG provider with DPI scaling.
- Three gate stages: Extension, Item, and Presentation.
- Built-in gates:
extensionPass,jsonFilter,presentationPass,demo:hideTemp,demo:readOnlyDisable; built-in executors:messageBox,launch,demo:actionLog. - Build output includes double-clickable
register.batandunregister.batscripts with UAC elevation; the scripts selectSystem32orSysWOW64regsvr32to match the DLL bitness. - Debug logging through
OutputDebugStringwith the[ShellExt]prefix. - Ships CTest unit tests covering config parsing, placeholder expansion, context building, launch command resolution, registration config parsing, and COM exception boundaries; CI runs them automatically.
- Windows x64
- Visual Studio 2026 C++ toolchain
- CMake 3.20 or later
- Python 3.9 or later for configuration validation
Windows 7 SP1 and later are the design targets. Validate the final DLL on the Windows version and Explorer bitness you intend to support.
cmake -S . -B build -G "Visual Studio 18 2026" -A x64 -DSHELLEXT_BUILD_TESTS=ON
cmake --build build --config Release
ctest --test-dir build -C Release --output-on-failure
python tools/validate_menu_json.pyUnit tests are off by default (SHELLEXT_BUILD_TESTS=ON enables them). They live in tests/ and compile the real sources instead of copies.
The DLL links the MSVC CRT statically (/MT), so the target machine does not need a separate VC++ runtime; the DLL is slightly larger.
tools/generate-vs.ps1 can also be used to generate the Visual Studio project.
The output is in build/bin/Release/:
CppShellExtContextMenuHandler.dll
menu.json
registration.json
register.bat
unregister.bat
icons/
Double-click register.bat in the DLL output directory. It uses the DLL beside the batch file and requests elevation before calling regsvr32. Double-click unregister.bat to remove the extension.
Alternatively, use an elevated PowerShell:
.\tools\register.ps1 -Action register
.\tools\register.ps1 -Action unregisterregister.ps1 validates the JSON syntax and registration targets (and runs the full validator when Python is available), copies registration.json, calls regsvr32, and copies menu.json and config/icons/ only after a successful registration; a failed registration restores the previous registration.json. After changing the DLL or configuration, unregister and register again; successful operations send SHChangeNotify, so an Explorer restart is normally unnecessary. An invalid registration.json fails registration without modifying the existing registry.
register.bat/unregister.bat and register.ps1 inspect the DLL's PE machine type and select the matching regsvr32 (System32 for x64, SysWOW64 for x86).
- Run
unregister.bat, then runregister.bat. - Right-click a regular file and verify
Demo -F. - Right-click a directory and verify
Demo -D. - Right-click empty space in a directory and verify
Demo -DB. - Right-click a drive root and verify
Demo -DR. - Select both a file and a directory and verify
Demo -FS. - Invoke an item and verify that the MessageBox shows the target name.
Mechanical rules come from RepoRules (checker vendored as scripts/repo_rules.py, configured by repo-rules.json) and run in CI via .github/workflows/hygiene.yml:
- C/C++ is UTF-8 BOM + CRLF; Markdown is CRLF; CMake, scripts, JSON and YAML are LF (see
.gitattributes/.editorconfig). - C/C++ lines stay within 140 columns; headers start with
#pragma once; no#pragma comment(lib)- link Windows libraries through CMake. CMakeLists.txt,src/resources/Version.rcandinclude/shell_ext/common.hmust agree on the version; CHANGELOG.md keeps an[Unreleased]section.- Relative Markdown links and the Chinese/English document pairs must resolve.
python scripts/repo_rules.py check --config repo-rules.json # check
python scripts/repo_rules.py fix --config repo-rules.json # repair BOM/line endings
python -m unittest discover -s scripts/tests -vOptionally install the pre-commit hook with git config core.hooksPath .githooks. See AGENTS.md for the rules and review priorities, CONTRIBUTING.en.md for the workflow, and SECURITY.en.md for private reporting.
config/registration.json:
{
"schemaVersion": 1,
"shellRegistrations": [
"file",
"directory",
"directoryBackground",
"drive",
"fileSystemObject"
]
}| Target | Registry location | Runtime context |
|---|---|---|
file |
* |
File-only selection |
directory |
Directory |
Directory-only selection |
directoryBackground |
Directory\\Background |
Empty selection in a directory |
drive |
Drive |
Drive root |
fileSystemObject |
AllFilesystemObjects |
Mixed file-and-directory selection |
If registration.json is missing, registration defaults to file. If the file exists but has an invalid schema, unknown target, duplicate target, or invalid array, registration fails instead of silently changing the registry. Unregistration removes all known targets so old registrations can be cleaned up.
{
"schemaVersion": 1,
"extensionGates": ["extensionPass"],
"itemGates": ["jsonFilter"],
"presentationGates": ["presentationPass"],
"executors": ["messageBox"],
"menuItems": []
}| Field | Description |
|---|---|
schemaVersion |
Optional; currently only 1 is accepted, so future format changes can reject incompatible files |
extensionGates |
Gate 1 chain; decides whether the extension participates |
itemGates |
Gate 2 chain; decides whether an item becomes a candidate; gates is an alias |
presentationGates |
Gate 3 chain; returns hidden, disabled, or enabled |
executors |
Default action executor chain |
menuItems |
Menu item definitions |
menu.json must be UTF-8 (BOM tolerated) and is parsed with third_party/nlohmann/json. Wrong field types, unknown gates/executors, unknown actionType, duplicate ids, and other document-level errors fall back to the built-in menu. Unknown chain names are not silently ignored.
Chain override semantics: an explicitly configured per-item chain replaces the root default entirely instead of appending to it. Root defaults apply only when the chain is missing or empty.
| Field | Description |
|---|---|
id |
Unique identifier |
label |
Menu text; & marks an accelerator |
verb |
String command name; InvokeCommand matches on it or canonicalName |
helpText |
Explorer status-bar help text |
canonicalName |
Canonical command name; accepted interchangeably with verb for matching |
icon |
SVG/BMP path relative to the DLL directory |
targets |
file, directory, directoryBackground, drive, or fileSystemObject |
separatorAfter |
Insert a separator after the item |
extensions |
Allowed extensions such as [".cpp", ".h"] |
excludeExtensions |
Extensions to reject |
minSelection / maxSelection |
Selection count; maxSelection: 0 means unlimited |
filesOnly / foldersOnly |
Restrict selected object types |
actionType |
messageBox or launch |
actionTitle / actionTemplate |
MessageBox title and content |
actionCommand / actionShowWindow |
Launch command and window option |
extensionGates / itemGates / presentationGates / executors |
Per-item overrides of root chains; explicit chains replace root defaults |
An empty targets array preserves compatibility with the generic selection filters. New items should set targets explicitly.
| Token | Meaning |
|---|---|
%1 |
Full path of the first selected object, or current directory |
%* |
All selected paths, quoted and joined |
%D |
Parent directory of the first selected object, or current directory |
%N |
Name of the first selected object or current directory, without its parent path |
If menu.json is missing or invalid, the extension falls back to the built-in item in include/shell_ext/common.h and the default gate chain.
Placeholder expansion is single-pass: the replacement text is not rescanned, so selected file names containing tokens such as %1 or %N are safe.
The launch action explicitly resolves the executable in actionCommand before starting it:
- Quoted paths are used as-is.
- Unquoted paths containing spaces follow the CreateProcess documented prefix-progressive search.
- Bare command names (such as
notepad.exe) are resolved from the system directories and absolute PATH entries only — the current directory is never searched, preventing same-name executables in it from being hijacked. - Unresolvable commands are logged and rejected instead of launched.
Selected object paths are not limited by MAX_PATH. %1, %*, %N, and %D are all processed as dynamic std::wstring values. Directory-background paths try SHGetPathFromIDListEx first and fall back to SHGetPathFromIDListW; file attribute reads retry with a \\?\ extended path when the plain path fails.
Actual paths longer than MAX_PATH still require the system long-path policy and a long-path-aware Explorer; otherwise the Shell will not hand such paths to the extension.
- Add an object to
menuItemsinconfig/menu.json. - Set a unique
id,verb,targets, filters, and action fields. - Put SVG/BMP assets in
config/icons/and reference them with a relative path. - Run
python tools/validate_menu_json.py.
- Add an implementation of
IExtensionGate,IMenuItemGate, orIMenuItemPresentationGateundersrc/gates/. - Register its name in
GateRegistry::RegisterBuiltInGates. - Use the name in the appropriate chain in
menu.json.
Extension gates decide whether the extension participates, item gates decide candidate membership, and presentation gates return Hidden, Disabled, or Enabled.
- Add an
IActionExecutorimplementation undersrc/actions/. - Register it in
ExecutorRegistry::RegisterBuiltInExecutors. - Add its name to a root or per-item
executorsarray. - Extend
MenuActionandMenuConfigif the executor needs new configuration fields.
- Add an enum value to
ShellTargetType. - Update
ParseShellTargetTypeandGetShellTargetRegistryFileType. - Add runtime recognition in
ContextBuilder. - Update target matching and the known registration/unregistration lists.
- Update
tools/validate_menu_json.py, README, and architecture docs.
Registration entry points are in src/extension/dllmain.cpp; registry helpers are in src/registry/Reg.cpp. Preserve rollback on partial registration, idempotent unregistration, and cleanup of legacy targets. Delete only this project's CLSID subkey, never an entire ContextMenuHandlers parent key.
- Use DebugView to inspect
[ShellExt]logs. - Match the DLL bitness to Explorer.
- Re-register after changing the DLL or configuration; restart Explorer when necessary.
- If a menu is missing, check registration targets, item
targets,filesOnly/foldersOnly, and gate chains together. - If registration fails, check elevation,
registration.json, and theregsvr32bitness.
config/ Runtime menu, registration targets, and icons
src/extension/ COM DLL entry point, ClassFactory, FileContextMenuExt, and ComGuard
src/context/ Selection context and Shell target recognition
src/gates/ Gate interfaces, registry, and built-in gates
src/actions/ Executor interfaces, registry, and actions
src/icons/ SVG/BMP icon providers
src/menu/ Configuration, filtering, insertion, and invocation
src/registry/ COM/Shell registry helpers
tools/ Build helpers, registration scripts, and validation
scripts/ Repository rules checker and its tests
tests/ CTest unit tests
third_party/ Third-party libraries (nlohmann/json, NanoSVG)
docs/ Architecture and validation documentation
- A 64-bit Explorer requires a 64-bit DLL; use the matching bitness for 32-bit Explorer. The registration scripts select
System32/SysWOW64regsvr32automatically. - COM registration requires administrator privileges.
- Windows 7 support is a design target; complete the Win7 SP1 x64 regression before release. The current VS 2026 toolchain and static CRT need separate validation on Win7; if unsupported, rebuild with a supported older toolchain.
- The DLL links the MSVC CRT statically; re-evaluate the runtime requirements if you add new dependencies.
- Do not edit generated files under
build/,build-*, or third-party directories.
When shipping a fork, change CLSID_FileContextMenuExt in src/extension/dllmain.cpp and the friendly names in include/shell_ext/common.h.
This project is derived from Microsoft's sample. Source files retain the original Microsoft headers and use the Ms-PL. The full license text is in LICENSE; third-party notices for nlohmann/json and NanoSVG are in THIRD_PARTY_NOTICES.md.