MiniPty 1.4.0
dotnet add package MiniPty --version 1.4.0
NuGet\Install-Package MiniPty -Version 1.4.0
<PackageReference Include="MiniPty" Version="1.4.0" />
<PackageVersion Include="MiniPty" Version="1.4.0" />
<PackageReference Include="MiniPty" />
paket add MiniPty --version 1.4.0
#r "nuget: MiniPty, 1.4.0"
#:package MiniPty@1.4.0
#addin nuget:?package=MiniPty&version=1.4.0
#tool nuget:?package=MiniPty&version=1.4.0
MiniPty
NativeAOT-friendly minimal cross-platform pseudo-terminal library for .NET.
Motivation
I needed a PTY library for NativeAOT projects, but existing .NET PTY libraries don't reliably work with NativeAOT. MiniPty is a minimal PTY library with a simple API, no third-party dependencies, and in-process backends built for AOT publish.
Benchmarks
You can check various benchmark patterns at GitHub Actions/Benchmark.
Ubuntu
<img src="https://raw.githubusercontent.com/guitarrapc/MiniPty/refs/heads/main/assets/benchmark-ubuntu.png" alt="Benchmark Ubuntu" width="600" />
Windows
<img src="https://raw.githubusercontent.com/guitarrapc/MiniPty/refs/heads/main/assets/benchmark-windows.png" alt="Benchmark Windows" width="600" />
macOS
<img src="https://raw.githubusercontent.com/guitarrapc/MiniPty/refs/heads/main/assets/benchmark-macos.png" alt="Benchmark macOS" width="600" />
Features
- NativeAOT ready: no winpty, and no helper process running beside a session (what ships per platform)
- Cross-platform: Windows, Linux, and macOS (FreeBSD: experimental)
- Scales to thousands of sessions: an idle session holds no thread and no output buffer (I/O completions on Windows, one epoll/kqueue loop on Unix)
- Idle sessions cost nothing: an idle session holds no thread and no output buffer (how each OS reads output)
- Bounded memory with backpressure: output goes into a 16 KiB buffer per session, and a slow consumer pauses the child instead of growing memory
- Allocation-free steady state: reading chunks, writing input, and waiting for exit allocate nothing after the first call
- Signals the right process: on Linux, signals go through a pidfd, so a reused PID is never hit
Not supported
- Remote shells (
ssh) or tunneling a PTY over the network - Full terminal emulation, TUI replay, or faithfully preserving
\roverwrite lines - Falling back to pipe redirect when PTY creation fails
Platforms
MiniPty creates a real PTY on each supported OS. See Backends for how each OS creates the PTY.
| OS | Architectures | Status |
|---|---|---|
| Windows 10 1809+ / Windows 11 | x64, arm64 | Supported |
| Linux | x64, arm64 | Supported |
| macOS | x64, arm64 | Supported |
| FreeBSD | — | Experimental: tested neither locally nor in CI, and the package ships no FreeBSD binary, so MiniPty works there only when built from source |
Native files
Your application carries the following besides the MiniPty assembly. The package and dotnet publish place these files for you.
| OS | Files beside the application | Under NativeAOT |
|---|---|---|
| Windows | None: ConPTY comes with Windows. The out-of-band ConPTY is optional and supplied by your application | None |
| Linux (x64, arm64) | libminipty_unix.so |
None: the shim is linked into the executable |
| macOS (x64, arm64) | libminipty_unix.dylib and minipty_spawn_helper |
minipty_spawn_helper only: the shim is linked into the executable, and the launcher must stay in the same directory, where MiniPty looks for it |
| FreeBSD (experimental) | libminipty_unix.so, which the package does not include: it is built with cc when MiniPty is built from source on FreeBSD |
Not supported by the package |
Quick start
dotnet add package MiniPty # PTY sessions
dotnet add package MiniPty.Capture # timestamped output capture
dotnet add package MiniPty.Console # attach the host terminal (vim, etc.)
dotnet add package MiniPty.Terminal # backend for xterm.js and editor terminals
MiniPty
Start a child with Pty.Start, then either run it for its result with CompleteAsync or drive it with WriteInputAsync and ReadOutputAsync. Try dotnet samples/SingleFiles/README_MiniPty.cs.
using MiniPty;
var shell = OperatingSystem.IsWindows() ? "cmd.exe" : "/bin/sh";
// Run a command for its result: CompleteAsync drains the output and waits for exit.
await using (var run = Pty.Start(new PtyStartInfo { FileName = shell, Arguments = [OperatingSystem.IsWindows() ? "/c" : "-c", "echo hello"] }))
{
var result = await run.CompleteAsync();
Console.WriteLine($"exit={result.ExitCode}: {PtyOutput.ToDisplayText(result.GetText(), PtyOutputDisplayMode.PlainText).Trim()}");
}
// Drive a session: write input, resize at any time, and stream output until the child exits.
await using var session = Pty.Start(new PtyStartInfo { FileName = shell, Size = new PtySize(120, 30) });
await session.WriteInputAsync("echo hello\r");
await session.WriteInputAsync("exit\r");
session.Resize(new PtySize(100, 40));
var stdout = Console.OpenStandardOutput();
await foreach (var chunk in session.ReadOutputAsync())
await stdout.WriteAsync(chunk.Data);
Console.WriteLine($"exit={(await session.WaitForExitAsync()).ExitCode}");
MiniPty.Capture
Run a child and get its output, exit code, and every read with its time since Pty.Start. Try dotnet samples/SingleFiles/README_MiniPty.Capture.cs.
using MiniPty;
using MiniPty.Capture;
var result = await PtyCapture.RunAsync(new PtyStartInfo
{
FileName = OperatingSystem.IsWindows() ? "cmd.exe" : "/bin/sh",
Arguments = [OperatingSystem.IsWindows() ? "/c" : "-c", "echo hello"],
});
foreach (var chunk in result.Chunks)
Console.WriteLine($"{chunk.Time.TotalSeconds:F3}s: {chunk.Data.Length} bytes");
Console.WriteLine(result.ToDisplayText(PtyOutputDisplayMode.PlainText));
MiniPty.Console
Attach the host terminal to a session, so interactive programs such as vim get your keyboard and window size. Reading output stays your job. Try dotnet samples/SingleFiles/README_MiniPty.Console.cs.
using MiniPty;
using MiniPty.Console;
await using var session = Pty.Start(new PtyStartInfo { FileName = OperatingSystem.IsWindows() ? "cmd.exe" : "/bin/bash" });
// Keyboard input and window resizes now go to the PTY.
using var input = PtyConsoleInput.Attach(session);
// MiniPty.Console does not read output: copy it to stdout yourself.
var output = CopyOutputAsync(session);
// Forward keys until the child exits.
using var exited = new CancellationTokenSource();
_ = session.WaitForExitAsync().ContinueWith(_ => exited.Cancel());
input.PumpInputUntil(exited.Token);
await output;
static async Task CopyOutputAsync(PtySession session)
{
var stdout = Console.OpenStandardOutput();
await foreach (var chunk in session.ReadOutputAsync())
{
await stdout.WriteAsync(chunk.Data);
await stdout.FlushAsync();
}
}
ConsoleAttach.cs adds status messages and keeps the shell prompt aligned afterwards.
MiniPty.Terminal
Serve a PTY to xterm.js or an editor terminal, the role node-pty plays behind VS Code. Try dotnet samples/SingleFiles/WebTerminal.cs for a shell in your browser.
using MiniPty;
using MiniPty.Terminal;
// webSocket: any System.Net.WebSockets.WebSocket (Kestrel, HttpListener, ...)
// ct: cancel it when the connection ends (for example HttpContext.RequestAborted)
var status = await PtyWebSocketBridge.RunAsync(
new PtyStartInfo { FileName = "/bin/bash", Size = new PtySize(120, 30) },
webSocket,
cancellationToken: ct);
Console.WriteLine($"shell exited: {status.ExitCode}");
Other entry points:
PtyWebSocketSessionManager: reconnectable browser sessions with authentication and replay.PtyStdioBridge: length-prefixed frames over stdio, for a helper process such as VsCodeTerminalHelper.cs behind the VS Code extension sample.PtyTerminal: a node-pty-style push API with pause/resume flow control, for your own transport.
Protocol and bridging details: terminal.md.
Backends
Each OS has its own backend behind the same API. None of them uses winpty, and none keeps a helper process running beside a session.
Windows: ConPTY
MiniPty calls Win32 ConPTY (CreatePseudoConsole) directly through P/Invoke, attaches the child with PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, and resizes with ResizePseudoConsole. Output is read through I/O completions on the runtime's thread pool, so an idle session holds no thread and no output buffer. An application can supply the out-of-band ConPTY (conpty.dll + OpenConsole.exe) per session with PtyStartInfo.ConPty; see Out-of-band ConPTY.
Linux: forkpty
MiniPty uses the small libminipty_unix native shim to call the platform PTY API, then execve the child inside the PTY. Resize uses TIOCSWINSZ. One background loop (the exit watcher's thread, on epoll) reads every session's output and writes its queued input, so an idle session holds no thread and no output buffer.
macOS: posix_spawn + launcher
MiniPty uses the Unix backend through libminipty_unix.dylib, and starts each child through minipty_spawn_helper (not forkpty): a small launcher that posix_spawn starts, which takes the terminal as its controlling terminal and then replaces itself with the child by exec, so no helper process stays running. Resize uses TIOCSWINSZ. Output and input go through the same one loop as on Linux, on kqueue.
FreeBSD (experimental): forkpty
MiniPty uses the Unix backend through libutil, matching the Linux/macOS PTY lifecycle.
Advanced Usage
Good to know
To use MiniPty effectively, keep the following points in mind.
- Keep reading output. A PTY nobody reads fills up and blocks the child.
ReadOutputAsync,CompleteAsync, and theOutputstream all drain it. - Disposing ends the child. Windows terminates it. Unix hangs it up (
SIGHUP, so a shell saves its history) and kills it if it has not exited a second later (Session contract). Environmentoverlays the parent's. A null value removes a variable. On Unix,TerminalNamesetsTERM, which defaults toxterm-256color(Spawn environment).- MiniPty is not a sandbox. The child runs with your permissions. If a program name, path, or argument comes from untrusted input, read Untrusted program names and paths and Batch files first.
Out-of-band ConPTY for Windows
Building a terminal emulator on Windows? Use the out-of-band ConPTY library instead of the in-box ConPTY.
In-box means the ConPTY that comes with Windows itself.
The in-box ConPTY drops graphics protocols (Sixel, Kitty) and some terminal queries, forwards mouse tracking only to programs that read VT input, and changes behavior with Windows updates. The ConPTY that Windows Terminal ships, published by Microsoft as Microsoft.Windows.Console.ConPTY, forwards everything and behaves the same on every Windows build.
# Use the out-of-band ConPTY library, 1.25 or later (older ones can stall three seconds at startup)
dotnet add package Microsoft.Windows.Console.ConPTY --version 1.25.260930003
MiniPty's build targets copy OpenConsole.exe beside conpty.dll for you. Both are native code that runs as your application, so ship them where your own binaries are protected, and take security fixes through package updates (details and measurements).
using MiniPty;
// Null on Unix, or when conpty.dll is not beside your application: the session then uses the in-box ConPTY.
var conpty = ConPtyLibrary.TryLoadFromApplicationDirectory();
if (conpty is { HasConsoleHost: false })
Console.Error.WriteLine("OpenConsole.exe is missing; sessions fall back to conhost.exe");
await using var session = Pty.Start(new PtyStartInfo { FileName = "cmd.exe", ConPty = conpty });
Windows options
KillProcessTree = true ends everything the child started, even programs in a console of their own, on dispose, on kill, and when your process crashes. Turn it on for commands run for their result (CI steps, test runners, agent tool calls). Leave it off for interactive terminals, where a window the user opened (code .) should outlive the tab (Process tree).
await using var session = Pty.Start(new PtyStartInfo
{
FileName = "cmd.exe",
Arguments = ["/c", "npm test"],
KillProcessTree = true,
});
InheritCursor = true keeps what the frontend already shows instead of letting ConPTY clear the screen. The frontend must then answer ConPTY's cursor position query, or the child waits (Inherited cursor).
Both options are ignored on Unix.
GUI hosts
WinUI, WPF, WinForms, and Avalonia can call every MiniPty API on the UI thread. Nothing blocks it, and no marshaling is needed.
// All on the UI thread.
var session = Pty.Start(new PtyStartInfo
{
FileName = shell,
Size = new PtySize(columns, rows),
ConPty = ConPtyLibrary.TryLoadFromApplicationDirectory(), // null on Unix, so the same line works everywhere
});
view.SizeChanged += (_, _) => session.Resize(new PtySize(view.Columns, view.Rows));
view.TextInput += async (_, text) => await session.WriteInputAsync(text);
window.Closed += async (_, _) => await session.DisposeAsync();
await foreach (var chunk in session.ReadOutputAsync())
view.Feed(chunk.Data.Span); // your VT parser and renderer
Full terminal emulators built this way: samples/WinUiTerminal (Windows) and samples/AvaloniaTerminal (Windows, Linux, macOS). See also GUI hosts.
Samples
See the VS Code extension sample instructions for complete Windows and macOS reproduction steps, including NativeAOT helper publishing and authenticated reconnect.
| Sample | Shows |
|---|---|
| Capture.cs | Minimal MiniPty.Capture smoke |
| Session.cs | Pty.Start, background Output reads, WriteInputAsync / SendEof, CompleteAsync, Resize |
| Interactive.cs | ReadOutputAsync persistent loop, marker-driven writes, mid-session Resize, natural child exit |
| ConsoleAttach.cs | MiniPty.Console host attach: keyboard → PTY, ReadOutputAsync → host display (requires interactive TTY) |
| Observe.cs | PtyCapture.RunAsync, per-read chunk timelines, stdin via PtyCaptureOptions.Completion |
| WebTerminal.cs | MiniPty.Terminal xterm.js in the browser: WebSocket bridge, ACK flow control, resize, exit banner, Ctrl+V paste and Ctrl+C copy of a selection |
| WebTerminalScale.cs | MiniPty.Terminal in an ASP.NET Core host serving many sessions: a dashboard of what each session costs the host (threads, memory, handles), and a bench mode that measures it with keystroke echo latency, and a flood's WebSocket frames per 32 KiB and host CPU per MiB |
| VsCodeTerminalHelper.cs | MiniPty.Terminal stdio-framed helper for a VS Code Pseudoterminal |
| VsCodeExtension | Dependency-free VS Code extension sample connecting Pseudoterminal to the NativeAOT helper |
| VsCodePersistentBridge.cs | Authenticated loopback service for real VS Code detach/reconnect testing |
| AvaloniaTerminal | The same terminal emulator as an Avalonia desktop app (Windows, Linux, macOS): DrawingContext grid, the same key, mouse, and clipboard handling, the same UI-thread MiniPty calls |
| WinUiTerminal | Code-only WinUI 3 terminal emulator (VT parser, screen buffer, Win2D grid, mouse copy/paste) driving Pty.Start, ReadOutputAsync, WriteInputAsync, Resize, and DisposeAsync from the UI thread (Windows only) |
Run a sample locally (JIT):
dotnet samples/SingleFiles/Session.cs
dotnet samples/SingleFiles/Interactive.cs
dotnet samples/SingleFiles/ConsoleAttach.cs
dotnet samples/SingleFiles/Observe.cs
dotnet samples/SingleFiles/Capture.cs
dotnet samples/SingleFiles/WebTerminal.cs # then open the http://localhost:5170/#token=... address it prints
dotnet run -c Release samples/SingleFiles/WebTerminalScale.cs # then open the http://localhost:5171/#token=... address it prints
dotnet run -c Release samples/SingleFiles/WebTerminalScale.cs -- bench --sessions 16,64,256
dotnet run --project samples/WinUiTerminal # Windows only
dotnet run --project samples/AvaloniaTerminal # Windows, Linux, macOS
NativeAOT publish (same flags as CI):
dotnet samples/SingleFiles/Session.cs -c Release --self-contained true -p:PublishAot=true -p:StripSymbols=true -p:DebugType=None
Development
Use dotnet for local development, debugging, or publishing.
Documentation
- Specification — API contracts, scope, document map, lessons learned
- Lifecycle — responsibility split, session flow diagram, cancellation, EOF, drain, disposal
- Implementation reference — ConPTY,
forkpty, EOF staging
Build
dotnet build
dotnet test
dotnet pack
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
NuGet packages (3)
Showing the top 3 NuGet packages that depend on MiniPty:
| Package | Downloads |
|---|---|
|
MiniPty.Capture
Timestamped PTY output observation for MiniPty. |
|
|
MiniPty.Console
Host terminal input attach for interactive MiniPty sessions. |
|
|
MiniPty.Terminal
Frontend terminal backend (xterm.js and editor integrations) for MiniPty. |
GitHub repositories
This package is not used by any popular GitHub repositories.