# CliWrap: a fluent process wrapper for .NET that replaces System.Diagnostics.Process

> CliWrap is an MIT-licensed C# library that models command-line execution as composable Command objects with configurable pipes. It targets .NET Standard 2.0 and up, and its last push was on 2026-09-01.

**Tyrrrz/CliWrap** — Library for interacting with command-line interfaces

- Repository: https://github.com/Tyrrrz/CliWrap
- Website: https://nuget.org/packages/CliWrap
- Stars: 5,001 · Forks: 286
- Language: C#
- License: MIT
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/tyrrrz-cliwrap

## What CliWrap actually solves for .NET developers

The .NET base class library gives you System.Diagnostics.Process, and that type makes you responsible for a long list of details: wiring stream readers, avoiding deadlocks when a child fills its output buffer, disposing handles, and deciding what to do when the process exits with a non-zero code. CliWrap's README describes the project as "a library for interacting with command-line interfaces" and claims an "airtight abstraction over System.Diagnostics.Process". That is the pitch in one line.

The audience is narrow and specific. You are writing C# or F#, you need to invoke an external executable (ffmpeg, git, a compiler, a cloud CLI), and you want to consume its stdout and stderr as data rather than as console noise. The library targets .NET Standard 2.0+, .NET Core 3.0+ and .NET Framework 4.6.2+, so it fits both modern and older codebases. It has no external dependencies, which matters if you are already fighting transitive package conflicts.

The README also carries a terms-of-use block stating that by using the project you implicitly agree to political statements about Russia and Ukraine. That is unusual for a NuGet package and worth knowing before you add it to a corporate dependency list, because someone in your organisation will eventually read the README and ask about it.

## Command objects, pipes, and the data flow behind ExecuteAsync

CliWrap's unit of work is a Command, not a process. You build one with Cli.Wrap("path/to/exe") and then configure it through a fluent interface: WithArguments, WithWorkingDirectory, and the pipe setters. The README states the design is built "with strict immutability in mind", so each configuration call returns a new command rather than mutating an existing one. That makes commands safe to store, share and reuse as values.

The mechanism underneath is still a child process. Calling ExecuteAsync spawns it, applies the arguments and working directory, and asynchronously waits for exit. What comes back is a CommandResult carrying ExitCode, IsSuccess, StartTime, ExitTime and RunTime. So the abstraction is not hiding the process model, it is wrapping it in an awaitable that returns a structured result instead of an event you have to subscribe to.

Streams are the interesting part. By default, the README says stdin, stdout and stderr are routed to CliWrap's equivalent of a null device: an empty input source and a sink that discards everything. You opt in to capturing data by attaching a PipeTarget, for example PipeTarget.ToStringBuilder(stdOutBuffer) for stdout and the matching call for stderr. The README explicitly notes that ExecuteBufferedAsync is the shorthand for this pattern, so the verbose pipe setup and the buffered extension are two spellings of the same data flow. That default is a deliberate trade-off: doing nothing means you never deadlock on a full pipe buffer, but it also means a process that fails silently prints nothing you can see unless you asked for it.

## Installing CliWrap and running a first command

Installation goes through NuGet, and the README gives a single command. Run it in the directory of the project that will call the external executable.

```bash
dotnet add package CliWrap
```

After the restore completes, the CliWrap namespace is available. The smallest useful program wraps an executable, passes arguments, and inspects the result. The README's quick overview uses exactly this shape.

```csharp
using CliWrap;

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

Replace path/to/exe with the real executable and the argument array with real flags. The task resolves to a CommandResult, so result.ExitCode is an int, result.IsSuccess is a bool, and result.RunTime is a TimeSpan. If the process exits non-zero, CliWrap throws rather than returning a failed result, which surprises people the first time.

For the common case of reading output, the buffered execution model is shorter. The README presents it as an extension method in the CliWrap.Buffered namespace.

```csharp
using CliWrap;
using CliWrap.Buffered;

var result = await Cli.Wrap("path/to/exe")
    .WithArguments(["--foo", "bar"])
    .ExecuteBufferedAsync();
```

Here result.StandardOutput and result.StandardError are strings decoded from the process streams. The README notes this implicitly configures pipes that write to in-memory buffers, so you get the capture behaviour without the StringBuilder boilerplate. What you should see after either snippet is a completed task and populated result fields, provided the executable exists and accepts the arguments.

## The non-zero exit code default is the first thing that bites

CliWrap throws an exception when the underlying process returns a non-zero exit code. The README flags this in a warning box and explains the reasoning: a non-zero code usually indicates an error. It also names the escape hatch, WithValidation(CommandResultValidation.None), which disables result validation and lets you inspect the exit code yourself.

This is the right default for a build tool or a package manager, where a failed command should abort the caller. It is the wrong default for anything that treats exit codes as data. Diff tools, linters, test runners and grep-like utilities use non-zero codes to mean "differences found" or "nothing matched", not "the process broke". If you wrap one of those and forget the validation call, your happy path throws.

The second constraint is subtler. Because the default pipes discard output, a command that fails with a useful error message on stderr will throw an exception whose text does not include that message. You configured a null device, so CliWrap has nothing to attach. The fix is to attach a stderr pipe or use ExecuteBufferedAsync before you start debugging, not after. The README documents the pipe configuration but does not, from what is shown, walk through this failure mode explicitly.

## How CliWrap compares with MedallionShell and CommandDotNet

The related searches point at three names worth separating, because they solve different problems.

MedallionShell is the closest alternative in spirit: a .NET library for running external processes with a friendlier API than raw Process. The difference in approach is where the abstraction sits. CliWrap's model is the immutable Command value plus explicit pipe targets, with cancellation handled through interrupt signals and complete ownership of the process lifetime claimed in the feature list. MedallionShell's API is built around shell-like command composition. If you want your C# to read like a pipeline of shell commands, that style will feel more natural; if you want each invocation to be a configured value you can pass around and reuse, CliWrap's model is the one that fits.

CommandDotNet is not a competitor at all. It goes the other direction: it builds a command-line application in C#, parsing arguments and dispatching to methods, rather than launching someone else's executable. Choosing between them is a question of whether your program is the CLI or the caller of one. The related searches also list Clifx, but nothing in the README describes it, so treat that name as unverified.

One more distinction worth making: CliWrap is not a shell. It wraps an executable path and an argument array. Shell features like globbing, variable expansion and pipelines between processes are not part of the model described in the README, so if your logic depends on them you are either invoking a shell explicitly or doing that work in C#.

## Licence, maintenance and the cost of upgrading

CliWrap ships under the MIT licence, with License.txt at the repository root. MIT is permissive: it allows commercial and closed-source use with attribution and no warranty. That is the general shape of the licence, not legal advice, and if your organisation has a policy on dependency licences you should read License.txt rather than this paragraph.

The repository is not archived, and the last push was on 2026-09-01. Recent releases are 3.10.5 on 2026-08-19, 3.10.4 on 2026-07-29 and 3.10.3 on 2026-07-24. The version numbers tell you the upgrade story: these are patch releases inside the 3.10 line, so the maintainer is fixing rather than reshaping the API. For a library that owns process lifetime and stream handling, that is the pattern you want, because breaking changes there force you to re-verify cancellation and pipe behaviour in your own code.

Upgrade cost is therefore low but not zero. A patch bump still means a new NuGet restore and a re-run of whatever integration tests exercise your external processes. The README offers Binternal for internalizing the library if you would rather not take an external dependency at all, which is a real option for teams that vendor their dependencies; the trade-off is that you then own the updates yourself. There is no migration guide in the README for moving between major versions, because the visible releases do not include a major bump.

## Conclusion

Adopt CliWrap when you need to launch external processes from .NET and want piping, cancellation and exit-code handling without writing Process boilerplate, especially if you already target .NET Standard 2.0 or later. Skip it when your integration is a single fire-and-forget process call, or when your project cannot take an external dependency and you are unwilling to internalize it with Binternal. Before committing, verify that the version on NuGet still carries the MIT terms in License.txt and check the WithValidation behaviour against the exit codes your own tooling actually returns, because CliWrap throws on non-zero by default.

## FAQ

### How do I install CliWrap in a .NET project?

Add the NuGet package with dotnet add package CliWrap, as the README's install section shows. The library targets .NET Standard 2.0+, .NET Core 3.0+ and .NET Framework 4.6.2+, so most current .NET projects can reference it.

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

That is the documented default: CliWrap treats a non-zero exit code as an error and throws. You can change it by calling WithValidation(CommandResultValidation.None) on the command before executing it.

### How do I capture stdout and stderr with CliWrap?

Attach pipes with WithStandardOutputPipe and WithStandardErrorPipe, for example PipeTarget.ToStringBuilder(stdOutBuffer), or use the ExecuteBufferedAsync extension method in the CliWrap.Buffered namespace, which configures in-memory buffers implicitly and exposes result.StandardOutput and result.StandardError.

### What does CliWrap return after a command finishes?

ExecuteAsync resolves to a CommandResult containing ExitCode, IsSuccess, StartTime, ExitTime and RunTime. The buffered variant adds the captured StandardOutput and StandardError strings.

### What is a CLI wrapper?

In this context it means a library that sits around an executable and gives you a structured way to launch it, feed its input, read its output and wait for it to finish. CliWrap performs that role for .NET by wrapping System.Diagnostics.Process.

## Sources

- [License: MIT](https://github.com/Tyrrrz/CliWrap/blob/prime/LICENSE)
- [Project website](https://nuget.org/packages/CliWrap)
- [README](https://github.com/Tyrrrz/CliWrap/blob/prime/README.md)
- [Releases](https://github.com/Tyrrrz/CliWrap/releases)
- [Tyrrrz/CliWrap on GitHub](https://github.com/Tyrrrz/CliWrap)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tyrrrz-cliwrap
