Tiny, cross-platform PTY library for Node.js, built in Zig, also usable as a standalone Zig package. Supports Linux, macOS, and Windows (via ConPTY).
Drop-in replacement for node-pty. 350x smaller (43 KB vs 15.5 MB packed, 176 KB vs 64.4 MB installed), no node-gyp or C++ compiler needed, and ships musl prebuilds for Alpine.
Regular child_process.spawn() runs programs without a terminal attached. That means no colors, no cursor control, no prompts — programs like vim, top, htop, or interactive shells simply don't work. A PTY (pseudo-terminal) makes the subprocess think it's connected to a real terminal. Colors, line editing, full-screen TUIs, and terminal resizing all work as expected.
- Terminal emulators — embed a terminal in Electron, Tauri, or a web app
- Remote shells — stream a PTY over WebSocket from a Node.js server
- CI / automation — run programs that require a TTY (interactive installers, REPLs)
- Testing — test CLI tools that use colors, prompts, or cursor movement
- AI agents — give LLM agents a real shell to run commands, observe output, and interact with CLIs
import { spawn } from "zigpty";
// auto-detects default shell ($SHELL on Unix, %COMSPEC% on Windows)
const pty = spawn(undefined, [], {
cols: 80,
rows: 24,
terminal: {
data(terminal, data: Uint8Array) {
process.stdout.write(data);
},
},
onExit(exitCode, signal) {
console.log("exited:", exitCode);
},
});
pty.write("echo hello\n");
pty.resize(120, 40);
await pty.exited; // Promise<number>Terminal callbacks bypass Node.js streams and deliver raw Uint8Array directly from native code. You can also use the onData/onExit event listeners instead:
pty.onData((data) => process.stdout.write(data));
pty.onExit(({ exitCode }) => console.log("exited:", exitCode));The Terminal class can be reused across multiple spawns and supports AsyncDisposable:
import { spawn, Terminal } from "zigpty";
await using terminal = new Terminal({
data(term, data) {
process.stdout.write(data);
},
});
const pty = spawn("/bin/sh", ["-c", "echo hello"], { terminal });
await pty.exited;
// terminal.close() called automatically by `await using`Spawn a process inside a new PTY.
Options:
interface IPtyOptions {
cols?: number; // Default: 80
rows?: number; // Default: 24
cwd?: string; // Default: process.cwd()
env?: Record<string, string>; // Default: process.env
name?: string; // Sets TERM (e.g. "xterm-256color")
encoding?: BufferEncoding | null; // Default: "utf8", null for raw Buffer
uid?: number; // Unix user ID
gid?: number; // Unix group ID
handleFlowControl?: boolean; // Intercept XON/XOFF (default: false)
terminal?: TerminalOptions | Terminal; // Bun-compatible terminal callbacks
onExit?: (exitCode: number, signal: number) => void;
}Returns:
interface IPty {
pid: number;
cols: number;
rows: number;
readonly process: string; // Foreground process name
readonly exited: Promise<number>; // Resolves with exit code
readonly exitCode: number | null; // Exit code or null if running
onData: (cb: (data: string | Buffer) => void) => IDisposable;
onExit: (cb: (e: { exitCode: number; signal: number }) => void) => IDisposable;
write(data: string): void;
resize(cols: number, rows: number): void;
kill(signal?: string): void; // Default: SIGHUP
pause(): void;
resume(): void;
close(): void;
waitFor(pattern: string, options?: { timeout?: number }): Promise<string>;
}Wait until the PTY output contains the given string. Returns all output collected so far. Useful for AI agents that need to read prompts before responding.
import { spawn, Terminal } from "zigpty";
// Terminal provides callback-based data handling and AsyncDisposable cleanup
await using terminal = new Terminal({
cols: 100,
rows: 30,
// Nice to meet you, zigpty! Zig is a great choice!
data: (_terminal, data) => process.stdout.write(data),
});
// spawn() attaches to the Terminal — data flows through terminal callbacks
const pty = spawn(
"python3",
[
"-c",
`
name = input("What is your name? ")
lang = input("Favorite language? ")
print(f"Nice to meet you, {name}! {lang} is a great choice!")
`,
],
{ terminal },
);
// waitFor() resolves when the output contains the pattern
await pty.waitFor("name?");
pty.write("zigpty\n");
await pty.waitFor("language?");
pty.write("Zig\n");
// exited returns a Promise<number> with the exit code
await pty.exited;Options: { timeout?: number } — default 30 seconds. Throws if the pattern is not found within the timeout.
Create a PTY pair without spawning a process — useful when you need to control the child process yourself.
import { open } from "zigpty";
const { master, slave, pty } = open({ cols: 80, rows: 24 });| Platform | Status |
|---|---|
| Linux x64 (glibc) | ✅ |
| Linux x64 (musl) | ✅ |
| Linux arm64 (glibc) | ✅ |
| Linux arm64 (musl) | ✅ |
| macOS x64 | ✅ |
| macOS arm64 | ✅ |
| Windows x64 | ✅ |
| Windows arm64 | ✅ |
All 8 platform binaries are prebuilt — no compiler needed at install time. On Linux, the native loader tries glibc first and falls back to musl automatically.
The PTY core is a standalone Zig package with no Node.js or NAPI dependency.
zig fetch --save git+https://github.com/pithings/zigpty.gitWire it up in build.zig:
const zigpty = b.dependency("zigpty", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("zigpty", zigpty.module("zigpty"));API:
const pty = @import("zigpty");
// Fork a process with a PTY
const result = try pty.forkPty(.{
.file = "/bin/bash",
.argv = &.{ "/bin/bash", null },
.envp = &.{ "TERM=xterm-256color", null },
.cwd = "/home/user",
.cols = 120,
.rows = 40,
});
// result.fd — PTY file descriptor (read/write)
// result.pid — child process ID
// Open a bare PTY pair (no process spawned)
const pair = try pty.openPty(80, 24);
// pair.master, pair.slave
// Resize
try pty.resize(result.fd, 80, 24, 0, 0);
// Foreground process name
var buf: [4096]u8 = undefined;
const name: ?[]const u8 = pty.getProcessName(result.fd, &buf);
// Block until child exits
const exit_info = pty.waitForExit(result.pid);
// exit_info.exit_code, exit_info.signal_codeRequires Zig 0.15.1+.
zig build # Build prebuilds (all targets)
zig build --release # Release build
bun run build # Build + bundle TypeScript
bun test # Run testsAPI-compatible with node-pty. Terminal API inspired by Bun.
MIT