CLI tool
Tyrrrz/CliWrap avatar
Tyrrrz/CliWrap

CliWrap turns Process.Start into an immutable, validating command

Library for interacting with command-line interfaces

5,011 stars285 forksC#MIT

At a glance

What is it?
CliWrap is a .NET library for launching processes and piping their streams, with validation that throws on a non-zero exit code unless you turn it off. It is MIT licensed, has no external dependencies, and ships with a terms of use you are agreeing to by using it.
Who is it for?
CliWrap fits a .NET codebase that shells out often and wants one place to build command lines, capture output and reason about exit codes. It does not fit code that treats a non-zero exit code as data without noticing, that streams large or binary output, or that has to clear its dependency list first.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 10 days ago.
What is it written in?
Mainly C#, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • Tyrrrz/CliWrap5,011 ★ · +13
Star history
Repository2026-09-232026-11-14
Tyrrrz/CliWrap5k5k

13 Stars in the last 52 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

The unit of work is a command, and validation is on by default

The base unit is a command object that encapsulates instructions for running a process, closer to a shell line than to a process handle. You build one with `Cli.Wrap(...)` and the executable path, configure it through the fluent interface, then run it with `ExecuteAsync()`:

csharp
using CliWrap;

var result = await Cli.Wrap("/path/to/cli")
    .WithArguments(["--foo", "bar"])
    .WithWorkingDirectory("/work/dir/path")
    .ExecuteAsync();

// Result contains:
// -- result.ExitCode        (int)
// -- result.IsSuccess       (bool)
// -- result.StartTime       (DateTimeOffset)
// -- result.ExitTime        (DateTimeOffset)
// -- result.RunTime         (TimeSpan)

The awaited task resolves to a `CommandResult` holding the exit code and timings. The behaviour to know before you write the first call is that a non-zero exit code throws, because it usually indicates an error. For tools where an exit code is information rather than failure, that default has to be undone with `WithValidation(CommandResultValidation.None)`, and forgetting it turns a normal result into an exception.

Unattached streams go to a null device, so output disappears quietly

By default the process's standard input, output and error streams are routed to the equivalent of a null device, which is an empty source and a target that discards everything written to it. Nothing is written to your console and nothing is captured, and nothing warns you about it. Attaching a pipe is the only way to see what the process printed:

csharp
using CliWrap;

var stdOutBuffer = new StringBuilder();
var stdErrBuffer = new StringBuilder();

var result = await Cli.Wrap("/path/to/cli")
    .WithArguments(["--foo", "bar"])
    .WithWorkingDirectory("/work/dir/path")
    // This can be simplified with `ExecuteBufferedAsync()`
    .WithStandardOutputPipe(PipeTarget.ToStringBuilder(stdOutBuffer))
    .WithStandardErrorPipe(PipeTarget.ToStringBuilder(stdErrBuffer))
    .ExecuteAsync();

The matching calls exist for input, output and error. The short form is `ExecuteBufferedAsync()` from the `CliWrap.Buffered` namespace, which sets up in-memory string buffers for you and puts `StandardOutput` and `StandardError` on the result. Its own warning is about memory: programs can write arbitrary data, including binary, to those streams, so buffering is impractical for large or binary output and the lower level piping options are there for that case.

Commands are immutable, so configuration builds rather than edits

The feature list describes the library as designed with strict immutability in mind, and the configuration section states that `Command` is an immutable object. Every option in the fluent chain therefore produces a command rather than editing one, which is why the examples read as a single expression from `Cli.Wrap` to `ExecuteAsync`.

The practical effect is on how you reuse configuration. A base command with a fixed executable and shared arguments cannot be re-pointed at a different working directory or a different argument later; you build the variant instead. That is a good trade when a command object is shared across threads or stored in a field, because there is no window in which another caller sees a half-configured command. It costs you the shortcut of mutating a prepared command, and it means a helper that takes a `Command` and tweaks it has to return the new one.

Cancellation is graceful, and lifetime ownership is the caller's

Four claims in the feature list describe how the library treats the child process: a fully asynchronous and cancellation-aware API, graceful cancellation using interrupt signals, complete ownership of the underlying process's lifetime, and safety against typical deadlock scenarios.

Two of those deserve a careful reading. Graceful cancellation means the request to stop is delivered as an interrupt signal. And deadlock safety is stated against typical scenarios, which is a claim about pipe handling rather than a promise about every program you launch.

One package, four target frameworks, and an option to vendor it

The package has no external dependencies, which is unusual for a library of this size and is stated as a feature rather than an accident. It targets .NET Standard 2.0+, .NET Core 3.0+ and .NET Framework 4.6.2+, and it is tested on Windows, Linux and macOS. Installing it is a single command:

bash
dotnet add package CliWrap

The wide target range is what makes one package usable from old and new projects.

The solution carries benchmarks, a signaler and a dummy app

The repository is more than the library. The top level holds `CliWrap/` for the library itself, `CliWrap.Tests/` for the test suite, `CliWrap.Benchmarks/` for performance work, `CliWrap.Tests.Dummy/` as a small application used as a test target, and `CliWrap.Signaler/` as a separate component. Packaging and build configuration sit alongside them in `Directory.Build.props`, `Directory.Packages.props`, `global.json` and `NuGet.config`, with the solution file named `CliWrap.slnx`.

Distribution happens through NuGet rather than GitHub releases as the primary channel, and the version numbers show a steady patch cadence: 3.10.3 on 2026-07-24, 3.10.4 on 2026-07-29 and 3.10.5 on 2026-08-19. The default branch is `prime`, the last push was on 2026-11-01, and the repository is not archived. For a consumer that means you install a version rather than tracking a branch, and the source tree you would fork includes test and benchmark infrastructure you did not ask for.

Using the library is framed as agreeing to four statements

The readme carries a terms of use section that is unusual for a package of this kind. It states that using the project or its source code comes with the implicit agreement that you condemn Russia's war of aggression against Ukraine, recognise Russia as a terrorist state, and support Ukraine's territorial integrity. A link is offered for background on the war.

The code itself is MIT licensed, so the licence question and the terms of use question are separate. An organisation that reviews open source dependencies by licence alone will not see this section, and a team that cannot accept those statements has to decide whether the package is usable at all. Development is funded by the community through a donation link, with a Discord server linked for questions.

Editorial conclusion

CliWrap fits a .NET codebase that shells out often and wants one place to build command lines, capture output and reason about exit codes. It does not fit code that treats a non-zero exit code as data without noticing, that streams large or binary output, or that has to clear its dependency list first. Before adopting, decide whether you want validation on, check the terms of use against your organisation's open source policy, and pin a NuGet version rather than tracking the default branch.

Frequently asked questions

What does CliWrap do in a .NET project?

CliWrap is a library for interacting with command-line interfaces. It provides a model for launching processes, redirecting input and output streams, awaiting completion and handling cancellation, built as an abstraction over System.Diagnostics.Process with a fluent configuration interface.

How do I install CliWrap?

Add the NuGet package with `dotnet add package CliWrap`. The package targets .NET Standard 2.0+, .NET Core 3.0+ and .NET Framework 4.6.2+, has no external dependencies, and is tested on Windows, Linux and macOS.

Why does CliWrap throw when the command exits with a non-zero code?

Result validation is on by default, and a non-zero exit code usually indicates an error, so CliWrap throws an exception. You can override that with `WithValidation(CommandResultValidation.None)` when the exit code is expected and carries information rather than failure.

How do I capture the output of a command with CliWrap?

By default the standard input, output and error streams go to a null device, so you must attach a pipe: `WithStandardInputPipe(...)`, `WithStandardOutputPipe(...)` and `WithStandardErrorPipe(...)`, for example with `PipeTarget.ToStringBuilder`. The `ExecuteBufferedAsync()` extension in the `CliWrap.Buffered` namespace does this with in-memory string buffers, which is impractical for large or binary output.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. Tyrrrz/CliWrap on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/tyrrrz-cliwrap.svg)](https://hysenlabs.com/projects/tyrrrz-cliwrap)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/tyrrrz-cliwrap.svg)](https://hysenlabs.com/projects/tyrrrz-cliwrap)

Was this useful?