diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json
index 33631d6569..85eb370159 100644
--- a/.pipelines/ESRPSigning_core.json
+++ b/.pipelines/ESRPSigning_core.json
@@ -65,6 +65,8 @@
"FancyZonesCLI.exe",
"FancyZonesCLI.dll",
+ "CliShim\\PowerToys.CliShim.exe",
+
"PowerToys.GcodePreviewHandler.dll",
"PowerToys.GcodePreviewHandler.exe",
"PowerToys.GcodePreviewHandlerCpp.dll",
diff --git a/PowerToys.slnx b/PowerToys.slnx
index 2d375dd426..ec28f3887e 100644
--- a/PowerToys.slnx
+++ b/PowerToys.slnx
@@ -1260,5 +1260,7 @@
+
+
diff --git a/doc/devdocs/cli-conventions.md b/doc/devdocs/cli-conventions.md
index a5bc4ec04b..349a611c8f 100644
--- a/doc/devdocs/cli-conventions.md
+++ b/doc/devdocs/cli-conventions.md
@@ -2,6 +2,32 @@
This document describes the conventions for implementing command-line interfaces (CLI) in PowerToys modules.
+## PATH-Visible Command Naming and Location
+
+- Name module CLI command shims `PowerToys..CLI.exe` (for example, `PowerToys.ImageResizer.CLI.exe`).
+- Install these shims in the `bin` subfolder of the PowerToys installation directory, which the installer adds to `PATH`.
+
+Every command is the same `PowerToys.CliShim.exe` payload (`tools/CliShim/`) installed under a different name. The shim resolves which CLI to launch from its own file name, forwards the raw argument tail unchanged, shares the caller's console, and returns the CLI's exit code. The CLI runs in a job object owned by the shim, so killing the shim kills the CLI with it; processes the CLI itself starts (the Settings window, for example) break away and survive.
+
+On a per-machine install the `bin` folder is created with a protected DACL (`MachinePathFolderSddl` in `installer/PowerToysSetupVNext/Common.wxi`) so that a custom installation root cannot leave a machine-`PATH` folder writable by standard users. Author that `` on the same component as the folder's `` `PATH` entry, so the two cannot drift apart.
+
+### Adding a new shim
+
+1. Add a `` item to `tools/CliShim/CliShimManifest.props` with the command name and the target's path relative to `bin`. Write that path with `/` separators, and against the *installed* layout (see [Signing and Deployment](#signing-and-deployment)) - which is where the CLI ends up, not where it is built from.
+2. Add the matching `` and `` to `installer/PowerToysSetupVNext/CliShims.wxs`, using the command name as the `File/@Name`.
+
+`CliShim.vcxproj` fails the build if the command names in those two drift apart, `build-installer.ps1` fails the build if a `RelativeTarget` does not resolve to a real executable, and `CliShim.UnitTests` generates its expectations from the same manifest, so there is no third list to update.
+
+### Shim exit codes
+
+The shim returns the target CLI's exit code unchanged. It substitutes one of its own codes only when the CLI never ran, using values outside the range the CLIs use themselves:
+
+| Code | Meaning |
+| --- | --- |
+| `9009` | No CLI is mapped to the invoked command name (matches `cmd.exe`'s "command not found"). |
+| `9010` | The mapped target executable is missing from the installation. |
+| `9011` | The shim could not start the target, including when it cannot resolve its own path. |
+
## Library
Use the **System.CommandLine** library for CLI argument parsing. This is already defined in `Directory.Packages.props`:
@@ -89,5 +115,5 @@ Reference implementations:
- CLI executables are signed automatically in CI/CD.
- **New CLI tools**: Add your executable and dll to `.pipelines/ESRPSigning_core.json` in the signing list.
-- CLI executables are deployed alongside their parent module (e.g., `C:\Program Files\PowerToys\modules\[ModuleName]\`).
+- CLI executables are deployed either to the installation root (e.g., `C:\Program Files\PowerToys\FancyZonesCLI.exe`) or, for WinUI 3 modules, next to their module in `WinUI3Apps\` (e.g., `C:\Program Files\PowerToys\WinUI3Apps\PowerToys.ImageResizerCLI.exe`). PATH-visible shims are deployed to `C:\Program Files\PowerToys\bin\`, and a shim's `RelativeTarget` is resolved from that `bin` folder against the *installed* layout - not against the source tree.
- Use self-contained deployment (import `Common.SelfContained.props`).
diff --git a/installer/PowerToysSetupVNext/CliShims.wxs b/installer/PowerToysSetupVNext/CliShims.wxs
new file mode 100644
index 0000000000..4c39ef2df1
--- /dev/null
+++ b/installer/PowerToysSetupVNext/CliShims.wxs
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/installer/PowerToysSetupVNext/Common.wxi b/installer/PowerToysSetupVNext/Common.wxi
index 21855a7936..f321875e40 100644
--- a/installer/PowerToysSetupVNext/Common.wxi
+++ b/installer/PowerToysSetupVNext/Common.wxi
@@ -54,5 +54,21 @@
+
+
+
diff --git a/installer/PowerToysSetupVNext/DscResources.wxs b/installer/PowerToysSetupVNext/DscResources.wxs
index 0566b13532..ff0b5b7cfe 100644
--- a/installer/PowerToysSetupVNext/DscResources.wxs
+++ b/installer/PowerToysSetupVNext/DscResources.wxs
@@ -28,7 +28,7 @@
-
+
diff --git a/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj b/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj
index 45cfbdcbaa..4a5c40aca8 100644
--- a/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj
+++ b/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj
@@ -136,6 +136,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
+
diff --git a/installer/PowerToysSetupVNext/Product.wxs b/installer/PowerToysSetupVNext/Product.wxs
index 8651a7d83d..0c2be9d701 100644
--- a/installer/PowerToysSetupVNext/Product.wxs
+++ b/installer/PowerToysSetupVNext/Product.wxs
@@ -67,6 +67,7 @@
+
@@ -299,6 +300,7 @@
+