This document describes the conventions for implementing command-line interfaces (CLI) in PowerToys modules.
- Name module CLI command shims
PowerToys.<ModuleName>.CLI.exe(for example,PowerToys.ImageResizer.CLI.exe). - Install these shims in the
binsubfolder of the PowerToys installation directory, which the installer adds toPATH.
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 <CreateFolder> on the same component as the folder's <Environment> PATH entry, so the two cannot drift apart.
- Add a
<CliShim>item totools/CliShim/CliShimManifest.propswith the command name and the target's path relative tobin. Write that path with/separators, and against the installed layout (see Signing and Deployment) - which is where the CLI ends up, not where it is built from. - Add the matching
<Component>and<ComponentRef>toinstaller/PowerToysSetupVNext/CliShims.wxs, using the command name as theFile/@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.
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. |
Use the System.CommandLine library for CLI argument parsing. The version is already defined in Directory.Packages.props:
<PackageVersion Include="System.CommandLine" Version="2.0.12" />Add the reference to your project:
<PackageReference Include="System.CommandLine" />- Use
--kebab-casefor long form (e.g.,--shrink-only). - Use single
-xfor short form (e.g.,-s,-w). - Pass the long name first, then any aliases:
new Option<bool>("--silent", "-s"). - Create options using
Option<T>and setDescriptionto descriptive help text. - Add validators (
option.Validators.Add(...), reporting failures withresult.AddError(...)) for options that require range or format checking.
- Create a
RootCommandwith a brief description. - Add all options and arguments to the command (
rootCommand.Options.Add(...),rootCommand.Arguments.Add(...)). RootCommandincludes the standard--helpand--versionoptions. Remove them fromrootCommand.Optionsif the CLI renders its own help or reuses their aliases.
- Use
rootCommand.Parse(args)to parse CLI arguments. - Extract option values using
parseResult.GetValue(option). - To run a handler, register it with
command.SetAction(parseResult => ...)and callparseResult.Invoke()orparseResult.InvokeAsync().
- On parse/validation errors, print error messages and usage, then exit with non-zero code.
Reference implementations:
- Awake:
src/modules/Awake/Awake/Program.cs - ImageResizer:
src/modules/imageresizer/ui/Cli/
- Provide a
PrintUsage()method for custom help formatting if needed.
- Consistency: Follow existing module patterns.
- Documentation: Always provide help text for each option.
- Validation: Validate input and provide clear error messages.
- Atomicity: Make one logical change per PR; avoid drive-by refactors.
- Build/Test Discipline: Build and test synchronously, one terminal per operation.
- Style: Follow repo analyzers (
.editorconfig, StyleCop) and formatting rules.
- Use
ManagedCommon.Loggerfor consistent logging. - Initialize logging early in
Main(). - Use dual output (console + log file) for errors and warnings to ensure visibility.
- Reference:
src/modules/imageresizer/ui/Cli/CliLogger.cs
- Every module CLI must record usage telemetry through the existing PowerToys telemetry infrastructure, respecting the user's diagnostic data consent.
- Record a fixed, allowlisted command category and a success flag based on the final exit code. Do not collect raw arguments, file paths, monitor IDs, profile values, or other user-provided content.
- Telemetry failures must not change command output, exit codes, or exception behavior.
- Add every new CLI telemetry event to DATA_AND_PRIVACY.md in the same PR, describing its purpose and collected data.
- Follow Telemetry Events for event naming, privacy tags, and implementation details.
0: Success1: General error (parsing, validation, runtime)2: Invalid arguments (optional)
- Always wrap
Main()in try-catch for unhandled exceptions. - Log exceptions before exiting with non-zero code.
- Display user-friendly error messages to stderr.
- Preserve detailed stack traces in log files only.
- Include tests for argument parsing, validation, and edge cases.
- Place CLI tests in module-specific test projects (e.g.,
src/modules/[module]/tests/*CliTests.cs).
- CLI executables are signed automatically in CI/CD.
- New CLI tools: Add your executable and dll to
.pipelines/ESRPSigning_core.jsonin the signing list. - 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 inWinUI3Apps\(e.g.,C:\Program Files\PowerToys\WinUI3Apps\PowerToys.ImageResizerCLI.exe). PATH-visible shims are deployed toC:\Program Files\PowerToys\bin\, and a shim'sRelativeTargetis resolved from thatbinfolder against the installed layout - not against the source tree. - Use self-contained deployment (import
Common.SelfContained.props).