56 results available
Sign in
Skills directory

system-commandline-cli

github

·

Last changed
Hardened

This skill has been hardened by Chainguard

Last changed

This version passed the hardening gate. Hardening applied 16 targeted changes and preserved all other text. Classified findings before hardening: 1. Classified findings closed: 0. Classified findings remaining: 1. Unclassified scanner findings remaining: 0. Review findings closed: 15. Review findings remaining: 1.


Open before hardening

1

Medium
Open after hardening

1

Medium

Expand
File
Group
Subtype
Severity
Status
SKILL.md

Excessive Agency & Rogue Autonomy

—

Medium
Open

1 finding


SKILL.md

System.CommandLine CLI Developer Skill

You are working on a .NET CLI application built with System.CommandLine v2.x.x, targeting .NET 8 or later or any .NET Standard 2.0 implementation, including .NET Framework 4.6.1 or later and .NET Core 2.0 or later. Follow these rules and patterns when creating or modifying CLI commands, unless the project already has established conventions or security practices, in which case preserve them. Pin one exact stable release of System.CommandLine in the project, using a lock file or central package management, and prefer currently supported runtimes such as .NET 8 or later for new work. The .NET Standard 2.0 compatibility above describes the library, not a recommendation to target out-of-support runtimes.


Architecture Overview

<CLI Project>/
├── Program.cs                       # Entry point and command invocation
└── Commands/
    ├── CommandBase.cs               # Base class for all commands
    ├── GlobalOptions.cs             # Defines global options for the CLI
    ├── RootCommand.cs               # Registers top-level commands
    └── <Group>/                     # One folder per command group
        ├── <Group>Command.cs        # Parent command that registers its children
        └── <Group><Verb>Command.cs  # Leaf command with its handler

RULE 1 — Prefer a Project-Specific Command Base Class

Prefer defining a project-specific abstract CommandBase that inherits from System.CommandLine.Command. Concrete commands should inherit from this base class so shared behavior and conventions remain centralized.

internal abstract class CommandBase : Command
{
    protected CommandBase(string name, string? description = null)
        : base(name, description)
    {
    }
}

internal sealed class MyCommand : CommandBase
{
    public MyCommand()
        : base("command-name", "Help text shown in --help")
    {
        this.SetAction(CommandHandler);
    }

    private async Task<int> CommandHandler(
        ParseResult parseResult,
        CancellationToken cancellationToken)
    {
        // implementation
        return 0;
    }
}

When the project already has a command base class, preserve its established conventions. Otherwise, introduce one when commands need shared behavior; simple applications may inherit from Command directly when a base class adds no meaningful value.


RULE 2 — Options and Arguments

Defining Options

private readonly Option<string> _myOption;

// In constructor:
_myOption = new Option<string>("--my-option")
{
    Description = "Clear description of what this option does",
    Required = true,   // or false
};
_myOption.Aliases.Add("-m");       // Add a short alias
this.Options.Add(_myOption);

Defining Arguments (positional)

private readonly Argument<string> _fileArgument;

// In constructor:
_fileArgument = new Argument<string>("file")
{
    Description = "Path to the input file"
};
this.Arguments.Add(_fileArgument);

Validate file arguments before use: add a validator to the argument's Validators collection that rejects missing files, resolve the value to a full canonical path, and where a base directory applies, reject paths that resolve outside it, including through symbolic links.

Reading Values in Handlers

// Required option/argument — use GetValue:
var value = parseResult.GetValue(_myOption);

Reading an omitted optional option or argument returns null or the default value. Declare optional options with nullable types and handle the missing case in the handler; mark options that must be supplied as Required.


RULE 3 — Command Handler Pattern

Handlers are async methods wired via SetAction:

this.SetAction(CommandHandler);

private async Task<int> CommandHandler(ParseResult parseResult, CancellationToken cancellationToken)
{
    // 1. Read option/argument values
    // 2. Load session settings (if needed)
    // 3. Validate configuration early — fail fast with clear error
    // 4. Execute business logic
    // 5. Output results to standard output and errors to standard error, with secrets redacted

    return 0; // or non-zero exit code
}

Pass the cancellation token to every awaited service call. When a handler awaits nothing, return a completed task instead of marking the method async. Redact keys, tokens, authorization headers, and other sensitive fields from results, service responses, and exception messages before writing them to the console.


RULE 4 — Command Group (Parent with Subcommands)

A group command registers children but does not call SetAction:

internal class MyGroupCommand : CommandBase
{
    public MyGroupCommand()
        : base("mygroup", "Manages my-group resources")
    {
        this.Subcommands.Add(new MyGroupListCommand());
        this.Subcommands.Add(new MyGroupCreateCommand());
        this.Subcommands.Add(new MyGroupDeleteCommand());
    }
}

A command may define both an action and subcommands when the direct invocation has meaningful behavior.


RULE 5 — Registration

  • Top-level commands → register in RootCommand.cs:

    this.Subcommands.Add(new MyGroupCommand());
  • Subcommands → register inside the parent command's constructor:

    this.Subcommands.Add(new MyGroupCreateCommand());

If the project defines its own root command class, give it a name distinct from the library's root command type, or fully qualify the library type, so the two types are not confused.


RULE 6 — User Confirmation for Destructive Operations

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")
{
    Console.WriteLine("Operation cancelled.");
    return 0;
}

Accept only the answers the prompt advertises, and compare the answer case-insensitively and culture-invariantly. Read confirmation only from an interactive console; when input is redirected or not interactive, stop with a non-zero exit code unless the user passed an explicit confirmation option on the command line. Adapt the sample so a cancelled operation returns a non-zero exit code that is distinct from success.


RULE 7 — Command Logic

The logic of each command should be in one or more service classes that implement interfaces. The command receives interfaces through dependency injection (DI), not concrete implementations. The command handler should not contain business logic. The command handler should be thin, responsible only for:

  1. Parsing input
  2. Validating configuration
  3. Calling the service method
  4. Outputting results

Service class should be injected in the command constructor via DI, not instantiated directly.


RULE 8 — Dependency Injection

Services are registered in Program.cs:

serviceCollection.TryAddSingleton<IMyService, MyServiceImpl>();

Add a convenience extension in ServiceProviderExtensions.cs:

public static IMyService GetMyService(this ServiceProvider provider)
    => provider.GetRequiredService<IMyService>();

Use this extension only in composition-root code at the program entry point. Commands receive services through constructor parameters as described in RULE 7, so construct commands that need services through the service container, or have the parent command pass their services in when it creates them.


RULE 9 — Naming Conventions

ElementConventionExample

CLI command name

lowercase kebab-case

agent create, set show

Command class

PascalCase + Command suffix

AgentCreateCommand

Option field

_camelCaseOption (private readonly)

_projectNameOption

Option long name

--kebab-case

--project-name

Option short alias

-x (single character)

-p

Argument field

_camelCaseArgument

_fileArgument

Namespace

MyProject.Commands.<Group>

MyProject.Commands.Agent

Folder

Commands/<Group>/

Commands/Agent/


RULE 10 — Visibility

  • All command classes are internal.

RULE 11 — Global Options and Validation

Define options shared by the entire command tree once in GlobalOptions.cs. Reuse the same Option<T> instance when registering, validating, and reading the option.

internal static class GlobalOptions
{
    public static readonly Option<string> EndpointOption = CreateEndpointOption();

    private static Option<string> CreateEndpointOption()
    {
        var option = new Option<string>(...);

        // add option description, aliases, and Required flag
        // Add validation to the option's Validators collection

        return option;
    }
}

Using Global Options in a Command

Expose repeated parsing or conversion through protected CommandBase helpers:

/// <summary>Resolves the validated endpoint from the global option.</summary>
protected Uri GetEndpoint(ParseResult parseResult)
{
    var baseUrl = parseResult.GetValue(GlobalOptions.EndpointOption)!;
    return new Uri(baseUrl);
}

/// <summary>Resolves the optional key from the global option.</summary>
protected string? GetKey(ParseResult parseResult)
    => parseResult.GetValue(GlobalOptions.KeyOption);

Consume those helpers from the leaf command's handler. The command must not add the global options to its own Options collection; recursive registration on the root already makes them available in its ParseResult.

private async Task<int> CommandHandler(
    ParseResult parseResult,
    CancellationToken cancellationToken)
{
    var endpoint = GetEndpoint(parseResult);
    var key = GetKey(parseResult);

    ...

    return 0;
}

Read a global option directly in a leaf handler only when no shared conversion or fallback logic is needed. Always use the static GlobalOptions symbol; never create a second Option<T> with the same aliases.

Follow these requirements:

  1. Set Recursive = true so the option is accepted for every descendant command.
  2. Add each global option exactly once to RootCommand.Options; do not duplicate it on leaf commands.
  3. Read values through the shared static symbol defined in the global options class, preferably behind a CommandBase helper.
  4. Add validation to the option's Validators collection so invalid input becomes a parse error and the command handler is not invoked. Do not rely on exceptions from new Uri(...) or downstream services.
  5. Validate endpoint options as nonblank absolute https URIs. Allow http only for local loopback development behind an explicit opt-in, and send the key only to https endpoints. Default the endpoint to a trusted value defined by the project, optionally restrict hosts to a configured allow list, and require explicit user confirmation before sending a key to a host other than the default. Reject unsupported schemes, relative URIs, query strings, and fragments because appending a fixed endpoint path would change their meaning.
  6. For optional secret options such as --key, allow omission but reject an explicitly supplied blank or whitespace-only value. Validate the value without logging, displaying, trimming, or otherwise mutating it. Secrets passed on the command line are visible in process listings, shell history, and CI logs, so prefer a source outside the command line that the project already supports and keep the option as a discouraged fallback. Apply the blank value check to every source, and keep the secret out of validation messages, help output, and logs.
  7. Keep validation separate from derivation.
  8. Use stable, actionable validation messages that name the option and the accepted format.
  9. Test global options through the root parser, including the default of each optional option, explicit valid values, invalid values, and placement before and after a representative subcommand. Verify invalid input prevents handler execution.

RULE 12 — Checklist for New Commands

When creating a new command, verify:

  1. ✅ Inherits from the project’s command base class when one exists or provides meaningful shared behavior
  2. ✅ Constructor passes name, description to base
  3. ✅ All options have Description, Required
  4. ✅ Handler wired via this.SetAction(CommandHandler)
  5. ✅ Handler signature: async Task<int> CommandHandler(ParseResult, CancellationToken)
  6. ✅ Command registered in parent (RootCommand or group command)
  7. ✅ Class is internal
  8. ✅ File placed in Commands/<Group>/ folder
  9. ✅ Namespace matches folder: MyProject.Commands.<Group>

Install

Install with chainctl
chainctl skill install public/github/system-commandline-cli:latest
Get started with chainctl
View source

Harden your own skills

Bring Chainguard's hardening pipeline to your first-party skills — custom rulesets, automated remediation, full audit trail.

Provenance

Upstream sourcehttps://github.com/github/awesome-copilot
Upstream commit258414c276b20f32248aec52628a10874239e2fe
OCI reference

skills.chainreg.biz/public/github.com/github/skills/system-commandline-cli:latest

License

MIT

Files

HARDENING.md

LICENSE

SKILL.md


The trusted source for open source

Talk to an expert
PrivacyTerms

Product

Chainguard ContainersChainguard LibrariesChainguard VMsChainguard OS PackagesChainguard ActionsChainguard Agent SkillsIntegrationsPricing
© 2026 Chainguard, Inc. All Rights Reserved.
Chainguard® and the Chainguard logo are registered trademarks of Chainguard, Inc. in the United States and/or other countries.
The other respective trademarks mentioned on this page are owned by the respective companies and use of them does not imply any affiliation or endorsement.