mrkeyoor.com_
Sat 08 Aug 21:56 UTC
npmCLI & Toolingupdated 08 Aug 2026

node-pty

node-pty is a native Node.js binding for starting a process inside a pseudoterminal instead of ordinary stdin and stdout pipes. That distinction makes shells, REPLs, full-screen terminal programs, color detection, cursor control, and window resizing behave as if a person opened a real terminal. It exposes output events plus write, resize, pause, resume, clear, and kill methods across Linux, macOS, and modern Windows ConPTY. It provides the process side of a terminal; a browser UI still needs a renderer such as xterm.js and a transport such as WebSocket.

Verdict

node-pty is the right low-level dependency when software genuinely needs terminal semantics, and its use in VS Code is meaningful evidence. Do not pay the native-build and security costs for routine subprocess execution, and isolate any shell reachable over a network.

API stability4/5The core spawn, onData, onExit, write, resize, pause, resume, clear, and kill surface is compact and established, with bundled TypeScript declarations. Platform details can still shift beneath that API: winpty was removed, flow control and several ConPTY options are marked experimental, and supported Node versions follow VS Code rather than a fixed long-term matrix.
Docs4/5The README covers a working cross-platform spawn, native build prerequisites, Windows requirements, flow control, security, thread safety, and a specific PowerShell environment failure. The complete API lives in the declaration file rather than a browsable reference site, and production topics such as WebSocket backpressure and lifecycle cleanup are left to the integrator.
Maintenance5/5Stable version 1.1.0 was published in December 2025, the repository was pushed on August 7, 2026, and a 1.2 beta line is active. Microsoft maintains the project as infrastructure for VS Code, with CI and current platform work, although native operating-system bindings naturally carry more maintenance exposure than a JavaScript-only package.
Ecosystem5/5node-pty records 3,174,223 weekly downloads, 2,000 GitHub stars, and direct use in VS Code, Hyper, Theia, browser terminals, editors, and desktop tools listed by the project. It pairs naturally with xterm.js and Electron, while its standard PTY concepts make transport and renderer integrations widely understood.

Use it if

  • You are building an IDE, terminal emulator, browser shell, or test runner that must behave like a real TTY
  • A child program changes behavior when stdout is a pipe and you need its colors, prompts, control sequences, or full-screen mode
  • You need to send terminal resize events as a user changes the visible rows and columns
  • You need one pseudoterminal API across Linux, macOS, and Windows ConPTY
Skip it if

Setup reality

npm install node-pty looks ordinary, but this is a native add-on. Version 1.1.0 ships prebuilt binaries for macOS x64 and arm64 plus Windows x64 and arm64. Its install script runs node-gyp rebuild when a matching prebuild directory is absent, which includes Linux in the published stable tarball. Linux builders need Python, make, and a C++ toolchain; macOS builders need Xcode; Windows source builds need Python, Visual C++, the Windows SDK, and sometimes the matching Spectre-mitigated libraries. The README sets the floor at Node.js 16 or Electron 19 and ties ongoing support largely to the runtime used by VS Code. Windows requires ConPTY on Windows 10 version 1809, build 18309, or later. At runtime, choose the shell separately for Windows and Unix, pass a complete environment, and preserve SystemRoot on Windows or PowerShell can fail with error 8009001d. Output is a terminal byte stream containing ANSI control sequences, not clean command output. Wire onData to a terminal renderer, send user input back with write, propagate viewport changes with resize, and always dispose listeners and kill abandoned processes. A networked terminal also needs authentication, command authorization, session limits, idle cleanup, output backpressure, and container or user isolation because the spawned program has the same privilege level as the Node process.

Patterns

Spawn the platform's interactive shellspawn-user-shell

import os from 'node:os';
import * as pty from 'node-pty';

const shell = os.platform() === 'win32'
  ? (process.env.COMSPEC ?? 'powershell.exe')
  : (process.env.SHELL ?? 'bash');

const term = pty.spawn(shell, [], {
  name: 'xterm-256color',
  cols: 80,
  rows: 24,
  cwd: process.cwd(),
  env: process.env,
});

Choose the executable per platform and keep SystemRoot in the Windows environment so PowerShell can initialize correctly.

Stream PTY output to the current terminalstream-terminal-output

const outputSubscription = term.onData((data) => {
  process.stdout.write(data);
});

term.onExit(() => outputSubscription.dispose());

Data contains terminal control sequences and partial chunks; do not treat each event as a complete line.

Forward local keyboard inputsend-terminal-input

process.stdin.setRawMode?.(true);
process.stdin.resume();
process.stdin.on('data', (chunk) => term.write(chunk));

Raw mode forwards control keys immediately; restore your stdin mode during shutdown if this runs inside a larger application.

Start one interactive programrun-interactive-command

const repl = pty.spawn(process.execPath, ['--interactive'], {
  name: 'xterm-256color',
  cols: 100,
  rows: 30,
  cwd: process.cwd(),
  env: process.env,
});

repl.onData((data) => process.stdout.write(data));

Arguments are passed as an array; on Windows a pre-escaped command-line string is also accepted, but quoting then becomes your responsibility.

Propagate viewport changesresize-terminal

function resizeTerminal(cols, rows) {
  if (!Number.isInteger(cols) || !Number.isInteger(rows)) return;
  if (cols < 1 || rows < 1) return;
  term.resize(cols, rows);
}

resizeTerminal(120, 36);

Use character columns and rows, not pixel dimensions, and validate values received from an untrusted client.

Clean up when the PTY exitsobserve-process-exit

const dataSubscription = term.onData(sendToClient);
const exitSubscription = term.onExit(({ exitCode, signal }) => {
  dataSubscription.dispose();
  exitSubscription.dispose();
  closeClient({ exitCode, signal });
});

The event provides an exit code and an optional signal; dispose listeners so reconnecting clients do not accumulate handlers.

Kill an abandoned PTY sessionterminate-session

const idleTimer = setTimeout(() => {
  if (process.platform === 'win32') term.kill();
  else term.kill('SIGHUP');
}, 15 * 60 * 1000);

term.onExit(() => clearTimeout(idleTimer));

Passing a signal is unsupported on Windows and throws; call kill without one there.

Enable automatic XON/XOFF flow controlenable-flow-control

const term = pty.spawn(shell, [], {
  name: 'xterm-256color',
  cols: 80,
  rows: 24,
  env: process.env,
  handleFlowControl: true,
});

term.write('long-running-command\r');

Flow control is experimental. With it enabled, complete XOFF and XON messages pause and resume the PTY instead of reaching the child.

Use application-specific pause messagescustomize-flow-control

const term = pty.spawn(shell, [], {
  env: process.env,
  handleFlowControl: true,
  flowControlPause: '\x1b[PAUSE',
  flowControlResume: '\x1b[RESUME',
});

Custom values avoid collisions when child output may contain ordinary XON or XOFF bytes; matching occurs only when a message arrives whole.

Pause output while a client is congestedmanual-backpressure

term.onData((data) => {
  const accepted = socket.write(data);
  if (!accepted) term.pause();
});

socket.on('drain', () => term.resume());

pause and resume control PTY reading; connect them to the transport's real backpressure signal instead of buffering without a limit.

Bridge a PTY to an authenticated socketbridge-websocket

const output = term.onData((data) => socket.send(JSON.stringify({ type: 'data', data })));

socket.on('message', (raw) => {
  const message = JSON.parse(String(raw));
  if (message.type === 'input') term.write(String(message.data));
  if (message.type === 'resize') term.resize(message.cols, message.rows);
});

socket.on('close', () => {
  output.dispose();
  term.kill();
});

Authenticate and authorize before spawning, validate resize bounds, cap input and output, and isolate the child from the host.

Synchronize a cleared Windows terminalclear-conpty-buffer

function clearTerminal() {
  terminalRenderer.clear();
  term.clear();
}

clear only affects node-pty's internal buffer on Windows ConPTY; it is a no-op on other platforms.

Alternatives

PackageRegistryPick it when
execanpmChoose it for running commands with captured output, cancellation, and errors when the child does not need a real terminal
cross-spawnnpmChoose it for portable child_process spawning and Windows command resolution without PTY behavior
ssh2npmChoose it when the terminal session belongs on a remote SSH server and you need SSH authentication and channel management