Add, modify, or review .NET CLI commands built with System.CommandLine by applying project command-base conventions, options and arguments, SetAction handlers, RootCommand registration, global options, dependency injection, validation, naming, and destructive-operation confirmation. Use when the user mentions System.CommandLine, CommandBase, ParseResult, SetAction, RootCommand, subcommands, or asks to add a CLI verb.
Take a .NET CLI command request, transform it into project-consistent System.CommandLine command classes, handlers, options, arguments, DI services, registration, validation, and tests, and return a command implementation or review that preserves the existing CLI architecture.
When to invoke
"Add a new System.CommandLine command."
"Wire this command with SetAction and ParseResult."
"Register a subcommand under RootCommand."
"Review these CLI options and arguments."
"Add global options to a .NET CLI."
Applicability
Use for .NET CLI projects using System.CommandLine v2.x.x on .NET 8 or later, any .NET Standard 2.0 implementation, .NET Framework 4.6.1 or later, or .NET Core 2.0 or later. Do not use for general C# coding, web APIs, UI work, or non-CLI projects.
Project structure
Preserve the repository's existing structure. When adding a conventional command layout from scratch, use this shape:
Entry point, service registration, parser or root command invocation.
Commands/CommandBase.cs
Project-specific abstract base class for shared helpers and conventions.
Commands/GlobalOptions.cs
Static definitions for shared recursive options.
Commands/RootCommand.cs
Registers top-level command groups and root options.
Commands/<Group>/<Group>Command.cs
Parent command that registers children.
Commands/<Group>/<Group><Verb>Command.cs
Leaf command with options, arguments, handler, and service calls.
Command class patterns
Prefer a project-specific CommandBase inheriting from System.CommandLine.Command when shared behavior exists. Concrete command classes are internal and inherit the existing base class; simple applications may inherit from Command directly if a base class adds no value.
Register children with this.Subcommands.Add(...); do not call SetAction unless direct invocation has useful behavior.
Root command
Add global options once to RootCommand.Options and top-level groups to Subcommands.
Options, arguments, and handlers
Define options and arguments as private readonly fields so the same symbol is used for registration and parsing.
private readonly Option<string> _myOption;
private readonly Argument<string> _fileArgument;
_myOption = new Option<string>("--my-option")
{
Description = "Clear description of what this option does",
Required = true,
};
_myOption.Aliases.Add("-m");
this.Options.Add(_myOption);
_fileArgument = new Argument<string>("file")
{
Description = "Path to the input file"
};
this.Arguments.Add(_fileArgument);
this.SetAction(CommandHandler);
private async Task<int> CommandHandler(ParseResult parseResult, CancellationToken cancellationToken)
{
var value = parseResult.GetValue(_myOption);
var file = parseResult.GetValue(_fileArgument);
return 0;
}
Handler sequence:
Read option and argument values through parseResult.GetValue(...).
Load session settings when needed.
Validate configuration early with clear parse or command errors.
Call service methods; keep business logic out of the command handler.
Output results with Console or the project's output abstraction.
Return 0 for success and non-zero for failure.
Registration and dependency injection
Concern
Required pattern
Top-level command
Register in RootCommand.cs: this.Subcommands.Add(new MyGroupCommand(...));.
Subcommand
Register inside the parent constructor: this.Subcommands.Add(new MyGroupCreateCommand(...));.
Service logic
Put command logic in service classes behind interfaces; inject interfaces into command constructors.
Service registration
Register services in Program.cs, for example serviceCollection.TryAddSingleton<IMyService, MyServiceImpl>();.
Convenience access
Add ServiceProviderExtensions.cs helpers only when the project already uses that style: provider.GetRequiredService<IMyService>().
Do not instantiate service implementations directly inside command handlers. Preserve existing DI container conventions and lifetimes.
Global options
Define shared options once in GlobalOptions.cs and reuse the same Option<T> instance for registration, validation, and parsing.
internal static class GlobalOptions
{
public static readonly Option<string> EndpointOption = CreateEndpointOption();
private static Option<string> CreateEndpointOption()
{
var option = new Option<string>("--endpoint")
{
Description = "Absolute http or https endpoint.",
Recursive = true,
Required = true,
};
option.Validators.Add(result =>
{
var value = result.GetValueOrDefault<string>();
if (!Uri.TryCreate(value, UriKind.Absolute, out var uri) ||
(uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps) ||
!string.IsNullOrEmpty(uri.Query) ||
!string.IsNullOrEmpty(uri.Fragment))
{
result.AddError("--endpoint must be an absolute http or https URI without query or fragment.");
}
});
return option;
}
}
Requirement
Reason
Set Recursive = true
The option is accepted for every descendant command.
Add each global option exactly once to RootCommand.Options
Leaf duplication creates alias conflicts and inconsistent parsing.
Read through the static GlobalOptions symbol
A second Option<T> with the same aliases will not carry the parsed value.
Prefer CommandBase helpers such as GetEndpoint(ParseResult parseResult) and GetKey(ParseResult parseResult)
Shared conversion and fallback logic stays centralized.
Validate through Validators
Invalid input becomes a parse error and the handler is not invoked.
Endpoint validation
Accept nonblank absolute http or https URIs; reject unsupported schemes, relative URIs, query strings, and fragments.
Secret option validation
Optional secrets such as --key may be omitted, but explicitly blank or whitespace-only values are invalid; do not log, display, trim, or mutate them.
Tests
Exercise root parser defaults, explicit valid values, invalid values, and option placement before and after a representative subcommand; verify invalid input prevents handler execution.
Destructive operations
Prompt before irreversible or destructive work unless the project has a standard --yes or --force pattern.
Console.WriteLine("Are you sure you want to delete X? This action cannot be undone. (yes/no)");
var confirmation = Console.ReadLine();
if (confirmation?.ToLower() != "yes" && confirmation?.ToLower() != "y")
{
Console.WriteLine("Operation cancelled.");
return 0;
}
Keep confirmation in the command layer and destructive business behavior in the service layer.
Inherits from the existing project command base, or from Command only when no meaningful base exists.
Constructor passes command name and description to the base constructor.
Options and arguments have Description; required inputs set Required.
Handler is wired with this.SetAction(CommandHandler).
Handler signature is async Task<int> CommandHandler(ParseResult parseResult, CancellationToken cancellationToken).
Command is registered in the parent, either RootCommand or a group command.
Command class is internal and located in Commands/<Group>/.
Namespace matches the folder and existing project convention.
Business logic lives in injected services, not the handler.
Destructive actions require confirmation or the project's established force flag.
Gotchas
Do not duplicate global options: recursive root registration makes them available in descendants; duplicating Option<T> instances breaks parsing expectations.
Keep validation separate from derivation: parse validation should reject invalid input before handler execution; helper methods can derive Uri, keys, or settings from validated values.
Group commands are not leaf commands: a parent that only groups subcommands should not call SetAction.
Stable errors matter: validation messages should name the option and accepted format so tests and users can act on them.
Source compatibility terms
Retain these System.CommandLine symbols and examples when updating older command files: --kebab-case, MyProject.Commands.<Group>, Options, RULE, my-group, new Uri(...), option/argument, parseResult.GetValue(GlobalOptions.Endpoint), GetMyService, GlobalOptions.Endpoint, GlobalOptions.EndpointOption, GlobalOptions.KeyOption, KeyOption, MyGroupDeleteCommand, MyGroupListCommand, MyProject.Commands, and ServiceProvider.
Output template
## System.CommandLine result — <command or review>
**Status:** implemented | reviewed | needs changes | blocked
**Command path:** `<root> <group> <verb>`
**Files changed or reviewed:** `<Program.cs>`, `<Commands/...>`
### Command shape
| Element | Value |
| --- | --- |
| Class | `<CommandClass>` |
| Base | `CommandBase` or `Command` |
| Handler | `SetAction(CommandHandler)` |
| Options | `<Option<T> fields and aliases>` |
| Arguments | `<Argument<T> fields>` |
| Registration | `<RootCommand.cs or parent command>` |
### Validation
- Root parser global options: pass | fail | not applicable
- Handler prevents invalid input: pass | fail | not applicable
- Destructive confirmation: pass | fail | not applicable
- Tests/build: `<command and result>`
Quality gate
The command follows existing project conventions before introducing a new CommandBase or folder pattern.
All System.CommandLine symbols are reused consistently: Command, Option<T>, Argument<T>, ParseResult, SetAction, RootCommand, Subcommands, Validators, and Recursive.
Global options are defined once, registered once on RootCommand.Options, and read through GlobalOptions or CommandBase helpers.
Services are registered through DI in Program.cs and resolved according to project conventions.
Naming, folder, namespace, alias, and visibility conventions are satisfied.
Destructive operations have confirmation or an established explicit bypass.
Parser tests or the smallest existing build/test command validate the changed command path.
1---2name: system-commandline-cli-33description: Add, modify, or review .NET CLI commands built with System.CommandLine by applying project command-base conventions, options and arguments, SetAction handlers, RootCommand registration, global options, dependency injection, validation, naming, and destructive-operation confirmation. Use when the user mentions System.CommandLine, CommandBase, ParseResult, SetAction, RootCommand, subcommands, or asks to add a CLI verb.4---56<!-- Generated from harness/github-copilot/plugins/dotnet-desktop-development/skills/system-commandline-cli/SKILL.md by harness/claude-code/scripts/convert_from_copilot.py. Edit the source, not this file. -->78# System.CommandLine CLI development910Take a .NET CLI command request, transform it into project-consistent `System.CommandLine` command classes, handlers, options, arguments, DI services, registration, validation, and tests, and return a command implementation or review that preserves the existing CLI architecture.1112## When to invoke1314- "Add a new System.CommandLine command."15- "Wire this command with SetAction and ParseResult."16- "Register a subcommand under RootCommand."17- "Review these CLI options and arguments."18- "Add global options to a .NET CLI."1920## Applicability2122Use for .NET CLI projects using `System.CommandLine` v2.x.x on `.NET 8` or later, any `.NET Standard 2.0` implementation, `.NET Framework 4.6.1` or later, or `.NET Core 2.0` or later. Do not use for general C# coding, web APIs, UI work, or non-CLI projects.2324## Project structure2526Preserve the repository's existing structure. When adding a conventional command layout from scratch, use this shape:2728```text29<CLI Project>/30├── Program.cs31└── Commands/32 ├── CommandBase.cs33 ├── GlobalOptions.cs34 ├── RootCommand.cs35 └── <Group>/36 ├── <Group>Command.cs37 └── <Group><Verb>Command.cs38```3940| File | Responsibility |41| --- | --- |42| `Program.cs` | Entry point, service registration, parser or root command invocation. |43| `Commands/CommandBase.cs` | Project-specific abstract base class for shared helpers and conventions. |44| `Commands/GlobalOptions.cs` | Static definitions for shared recursive options. |45| `Commands/RootCommand.cs` | Registers top-level command groups and root options. |46| `Commands/<Group>/<Group>Command.cs` | Parent command that registers children. |47| `Commands/<Group>/<Group><Verb>Command.cs` | Leaf command with options, arguments, handler, and service calls. |4849## Command class patterns5051Prefer a project-specific `CommandBase` inheriting from `System.CommandLine.Command` when shared behavior exists. Concrete command classes are `internal` and inherit the existing base class; simple applications may inherit from `Command` directly if a base class adds no value.5253```csharp54internal abstract class CommandBase : Command55{56 protected CommandBase(string name, string? description = null)57 : base(name, description)58 {59 }60}6162internal sealed class MyCommand : CommandBase63{64 public MyCommand(IMyService service)65 : base("command-name", "Help text shown in --help")66 {67 this.SetAction(CommandHandler);68 }6970 private async Task<int> CommandHandler(71 ParseResult parseResult,72 CancellationToken cancellationToken)73 {74 return 0;75 }76}77```7879| Command type | Rule |80| --- | --- |81| Leaf command | Define options/arguments, call `this.SetAction(CommandHandler)`, parse values, validate early, call services, return exit code. |82| Group command | Register children with `this.Subcommands.Add(...)`; do not call `SetAction` unless direct invocation has useful behavior. |83| Root command | Add global options once to `RootCommand.Options` and top-level groups to `Subcommands`. |8485## Options, arguments, and handlers8687Define options and arguments as private readonly fields so the same symbol is used for registration and parsing.8889```csharp90private readonly Option<string> _myOption;91private readonly Argument<string> _fileArgument;9293_myOption = new Option<string>("--my-option")94{95 Description = "Clear description of what this option does",96 Required = true,97};98_myOption.Aliases.Add("-m");99this.Options.Add(_myOption);100101_fileArgument = new Argument<string>("file")102{103 Description = "Path to the input file"104};105this.Arguments.Add(_fileArgument);106107this.SetAction(CommandHandler);108109private async Task<int> CommandHandler(ParseResult parseResult, CancellationToken cancellationToken)110{111 var value = parseResult.GetValue(_myOption);112 var file = parseResult.GetValue(_fileArgument);113 return 0;114}115```116117Handler sequence:1181191. Read option and argument values through `parseResult.GetValue(...)`.1202. Load session settings when needed.1213. Validate configuration early with clear parse or command errors.1224. Call service methods; keep business logic out of the command handler.1235. Output results with `Console` or the project's output abstraction.1246. Return `0` for success and non-zero for failure.125126## Registration and dependency injection127128| Concern | Required pattern |129| --- | --- |130| Top-level command | Register in `RootCommand.cs`: `this.Subcommands.Add(new MyGroupCommand(...));`. |131| Subcommand | Register inside the parent constructor: `this.Subcommands.Add(new MyGroupCreateCommand(...));`. |132| Service logic | Put command logic in service classes behind interfaces; inject interfaces into command constructors. |133| Service registration | Register services in `Program.cs`, for example `serviceCollection.TryAddSingleton<IMyService, MyServiceImpl>();`. |134| Convenience access | Add `ServiceProviderExtensions.cs` helpers only when the project already uses that style: `provider.GetRequiredService<IMyService>()`. |135136Do not instantiate service implementations directly inside command handlers. Preserve existing DI container conventions and lifetimes.137138## Global options139140Define shared options once in `GlobalOptions.cs` and reuse the same `Option<T>` instance for registration, validation, and parsing.141142```csharp143internal static class GlobalOptions144{145 public static readonly Option<string> EndpointOption = CreateEndpointOption();146147 private static Option<string> CreateEndpointOption()148 {149 var option = new Option<string>("--endpoint")150 {151 Description = "Absolute http or https endpoint.",152 Recursive = true,153 Required = true,154 };155156 option.Validators.Add(result =>157 {158 var value = result.GetValueOrDefault<string>();159 if (!Uri.TryCreate(value, UriKind.Absolute, out var uri) ||160 (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps) ||161 !string.IsNullOrEmpty(uri.Query) ||162 !string.IsNullOrEmpty(uri.Fragment))163 {164 result.AddError("--endpoint must be an absolute http or https URI without query or fragment.");165 }166 });167168 return option;169 }170}171```172173| Requirement | Reason |174| --- | --- |175| Set `Recursive = true` | The option is accepted for every descendant command. |176| Add each global option exactly once to `RootCommand.Options` | Leaf duplication creates alias conflicts and inconsistent parsing. |177| Read through the static `GlobalOptions` symbol | A second `Option<T>` with the same aliases will not carry the parsed value. |178| Prefer `CommandBase` helpers such as `GetEndpoint(ParseResult parseResult)` and `GetKey(ParseResult parseResult)` | Shared conversion and fallback logic stays centralized. |179| Validate through `Validators` | Invalid input becomes a parse error and the handler is not invoked. |180| Endpoint validation | Accept nonblank absolute `http` or `https` URIs; reject unsupported schemes, relative URIs, query strings, and fragments. |181| Secret option validation | Optional secrets such as `--key` may be omitted, but explicitly blank or whitespace-only values are invalid; do not log, display, trim, or mutate them. |182| Tests | Exercise root parser defaults, explicit valid values, invalid values, and option placement before and after a representative subcommand; verify invalid input prevents handler execution. |183184## Destructive operations185186Prompt before irreversible or destructive work unless the project has a standard `--yes` or `--force` pattern.187188```csharp189Console.WriteLine("Are you sure you want to delete X? This action cannot be undone. (yes/no)");190var confirmation = Console.ReadLine();191if (confirmation?.ToLower() != "yes" && confirmation?.ToLower() != "y")192{193 Console.WriteLine("Operation cancelled.");194 return 0;195}196```197198Keep confirmation in the command layer and destructive business behavior in the service layer.199200## Naming conventions201202| Element | Convention | Example |203| --- | --- | --- |204| CLI command name | lowercase kebab-case | `agent create`, `set show` |205| Command class | PascalCase plus `Command` suffix | `AgentCreateCommand` |206| Option field | private readonly `_camelCaseOption` | `_projectNameOption` |207| Option long name | kebab-case with `--` | `--project-name` |208| Option short alias | one or two characters | `-p`, `-id`, `-md` |209| Argument field | private readonly `_camelCaseArgument` | `_fileArgument` |210| Namespace | project commands namespace plus group | `MyProject.Commands.Agent`, `MyProject.CLI.Commands.<Group>` |211| Folder | `Commands/<Group>/` | `Commands/Agent/` |212| Visibility | command classes are `internal` | `internal sealed class AgentCreateCommand` |213214## Checklist for new commands215216- [ ] Inherits from the existing project command base, or from `Command` only when no meaningful base exists.217- [ ] Constructor passes command `name` and `description` to the base constructor.218- [ ] Options and arguments have `Description`; required inputs set `Required`.219- [ ] Handler is wired with `this.SetAction(CommandHandler)`.220- [ ] Handler signature is `async Task<int> CommandHandler(ParseResult parseResult, CancellationToken cancellationToken)`.221- [ ] Command is registered in the parent, either `RootCommand` or a group command.222- [ ] Command class is `internal` and located in `Commands/<Group>/`.223- [ ] Namespace matches the folder and existing project convention.224- [ ] Business logic lives in injected services, not the handler.225- [ ] Destructive actions require confirmation or the project's established force flag.226227## Gotchas228229- **Do not duplicate global options**: recursive root registration makes them available in descendants; duplicating `Option<T>` instances breaks parsing expectations.230- **Keep validation separate from derivation**: parse validation should reject invalid input before handler execution; helper methods can derive `Uri`, keys, or settings from validated values.231- **Group commands are not leaf commands**: a parent that only groups subcommands should not call `SetAction`.232- **Stable errors matter**: validation messages should name the option and accepted format so tests and users can act on them.233234## Source compatibility terms235236Retain these System.CommandLine symbols and examples when updating older command files: `--kebab-case`, `MyProject.Commands.<Group>`, `Options`, `RULE`, `my-group`, `new Uri(...)`, `option/argument`, `parseResult.GetValue(GlobalOptions.Endpoint)`, `GetMyService`, `GlobalOptions.Endpoint`, `GlobalOptions.EndpointOption`, `GlobalOptions.KeyOption`, `KeyOption`, `MyGroupDeleteCommand`, `MyGroupListCommand`, `MyProject.Commands`, and `ServiceProvider`.237238## Output template239240```markdown241## System.CommandLine result — <command or review>242243**Status:** implemented | reviewed | needs changes | blocked244**Command path:** `<root> <group> <verb>`245**Files changed or reviewed:** `<Program.cs>`, `<Commands/...>`246247### Command shape248| Element | Value |249| --- | --- |250| Class | `<CommandClass>` |251| Base | `CommandBase` or `Command` |252| Handler | `SetAction(CommandHandler)` |253| Options | `<Option<T> fields and aliases>` |254| Arguments | `<Argument<T> fields>` |255| Registration | `<RootCommand.cs or parent command>` |256257### Validation258- Root parser global options: pass | fail | not applicable259- Handler prevents invalid input: pass | fail | not applicable260- Destructive confirmation: pass | fail | not applicable261- Tests/build: `<command and result>`262```263264## Quality gate265266- [ ] The command follows existing project conventions before introducing a new `CommandBase` or folder pattern.267- [ ] All `System.CommandLine` symbols are reused consistently: `Command`, `Option<T>`, `Argument<T>`, `ParseResult`, `SetAction`, `RootCommand`, `Subcommands`, `Validators`, and `Recursive`.268- [ ] Global options are defined once, registered once on `RootCommand.Options`, and read through `GlobalOptions` or `CommandBase` helpers.269- [ ] Handler code is thin: parse, validate, call service, output, return exit code.270- [ ] Services are registered through DI in `Program.cs` and resolved according to project conventions.271- [ ] Naming, folder, namespace, alias, and visibility conventions are satisfied.272- [ ] Destructive operations have confirmation or an established explicit bypass.273- [ ] Parser tests or the smallest existing build/test command validate the changed command path.
Run npx skillmds@latest add paulasilvatech/system-commandline-cli-3 in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Add, modify, or review .NET CLI commands built with System.CommandLine by applying project command-base conventions, options and arguments, SetAction handlers, RootCommand registration, global options, dependency injection, validation, naming, and destructive-operation confirmation. Use when the user mentions System.CommandLine, CommandBase, ParseResult, SetAction, RootCommand, subcommands, or asks to add a CLI verb. It is listed under Coding & Dev Tools on SkillMD.
This skill has not completed SkillMD's automated safety review yet. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
paulasilvatech (@paulasilvatech) published this skill. Their other Agent Skills are listed on their SkillMD profile.