Minimal cross-platform detached PTY — a pseudo-terminal that makes programs think they're running in a real terminal. Launch commands, attach/view/record later.
Status: Beta — fully functional on Windows and Linux. CLI flags may evolve before 1.0.
When you launch a long-running process (an AI agent, a build, a server) programmatically, you lose the terminal. The process runs headless — no TUI, no colors, no way to see or interact with it.
On Linux, screen or tmux solve this — but they do far too much. On Windows, nothing equivalent exists.
holdpty sits between nohup and screen:
- More than
nohup: preserves a real PTY so programs behave interactively (TUI, colors, line editing). - Less than
screen/tmux: no window management, no splits, no keybindings, no config files.
One thing, done well, composable with other tools.
holdpty is a spiritual successor to dtach (Ned T. Crigler) and abduco (Marc André Tanner) — minimal detach/attach tools for Unix. holdpty brings the same concept to the Node.js ecosystem with first-class Windows support via ConPTY.
Also worth noting: alden (mskala, 2025) takes a different approach — it uses named pipes instead of a PTY layer to relay the raw character stream, which preserves the outer terminal's native scrollback. The tradeoff is Linux-only and no output replay buffer.
npm install -g holdpty- Node.js 18+ and npm
- holdpty uses node-pty for cross-platform PTY support. On most systems, prebuilt binaries are included. If not:
- Windows: Visual Studio Build Tools (C++ workload)
- Linux:
build-essential,python3 - macOS: Xcode Command Line Tools
# Launch a command in a detached PTY (returns immediately)
holdpty launch --bg --name worker1 -- node server.js
# List running sessions
holdpty ls
# Attach interactively (Ctrl+A then d to detach)
holdpty attach worker1
# Watch from another terminal (read-only, multiple viewers allowed)
holdpty view worker1
# Dump the output buffer and exit (for scripts/agents)
holdpty logs worker1
# Stop a session
holdpty stop worker1| Command | Description | Stdout |
|---|---|---|
launch --bg |
Start session detached, return immediately | Session name |
launch --fg |
Start session in foreground (blocks until child exits) | Nothing |
attach <session> |
Interactive connection (single-writer) | Terminal takeover |
view <session> |
Read-only live stream (multiple viewers) | PTY data only |
logs <session> |
Dump output buffer to stdout, exit (supports --tail, --follow, --no-replay) |
PTY data only |
send <session> <text> |
Inject input without attaching (non-exclusive) | Nothing |
ls [--json] |
List active sessions (auto-cleans stale) | Session list |
stop <session> |
Send SIGTERM to child process | Confirmation (stderr) |
info <session> |
Show session metadata | JSON |
# --fg or --bg is required (no default — be explicit)
holdpty launch --bg --name myapp -- python train.py
holdpty launch --fg --name build -- make all
# Auto-generated name if --name omitted
holdpty launch --bg -- npm start
# Prints: npm-a3f2
# Set initial PTY dimensions (default: 120x40)
holdpty launch --bg --cols 200 --rows 50 -- /bin/zshThe -- separator before the command is optional. This is important for PowerShell, which strips -- before it reaches the process:
# Both work:
holdpty launch --bg --name worker1 -- node server.js
holdpty launch --bg --name worker1 node server.jsholdpty attach worker1
# You're now in the session. Your keystrokes go to the PTY.
# Detach: Ctrl+A then d
# The session keeps running after detach.Only one attachment at a time. If someone is already attached:
Error: session "worker1" has an active attachment. Use 'holdpty view worker1' for read-only access.
# Read-only live stream — see exactly what the PTY is rendering
holdpty view worker1
# Multiple viewers can connect simultaneously
# view outputs real terminal data (escape sequences, TUI, colors)
# Your terminal renders it — like watching over someone's shoulder# Dump the ring buffer contents and exit
holdpty logs worker1
# Last 50 lines only
holdpty logs worker1 --tail 50
# Follow: replay buffer + keep streaming live output (like tail -f)
holdpty logs worker1 --follow
holdpty logs worker1 -f --tail 50
# Live output only, skip buffer replay
holdpty logs worker1 -f --no-replay
# Pipe-friendly
holdpty logs worker1 --tail 20 | grep ERROR# Send text to a session without attaching (like tmux send-keys)
holdpty send worker1 "npm test"
# Send from a pipe
echo "exit" | holdpty send worker1 --stdin
# Multiple senders can run concurrently — no exclusive lock
# Works even while someone is attached or viewingUnlike attach, send does not take an exclusive writer lock. Multiple senders, an attached client, and viewers can all coexist. This makes send ideal for:
- Orchestration tools injecting commands programmatically
- CI/CD scripts that drive interactive sessions
- Multi-agent systems sending notifications
Note: Concurrent senders have no ordering or atomicity guarantees. If multiple senders write simultaneously, their input may interleave. Callers should serialize sends if ordering matters.
Default: Ctrl+A then d (same as GNU screen). Works on all keyboard layouts.
Press Ctrl+A twice to send a literal Ctrl+A to the process.
Configurable via HOLDPTY_DETACH — comma-separated hex bytes (Ctrl+A = 0x01, Ctrl+B = 0x02, d = 0x64):
export HOLDPTY_DETACH="0x02,0x64" # Ctrl+B then d (tmux-style)
export HOLDPTY_DETACH="0x1d,0x64" # Ctrl+] then d (telnet-style)Sessions are stored as Unix domain sockets + JSON metadata:
| Platform | Default path |
|---|---|
| Windows | %TEMP%\dt\ |
| Linux | $XDG_RUNTIME_DIR/dt/ or /tmp/dt-$UID/ |
Override: HOLDPTY_DIR environment variable.
Each session is a holder process that:
- Creates a PTY via
node-pty(ConPTY on Windows, forkpty on Linux/macOS) - Spawns the command inside it
- Buffers output in a 1MB ring buffer
- Listens on a Unix domain socket for client connections
- Relays data between clients and the PTY using a binary protocol
- Exits when the child process exits, cleaning up socket + metadata
Sessions are regular processes — they don't daemonize themselves. Use --bg for detached launch, or manage with pm2/systemd/nohup as needed. This is not a process manager.
| Command | Exit code |
|---|---|
launch --bg |
0 on successful launch |
launch --fg |
Child's exit code |
attach |
Child's exit code if child exits while attached; 0 on detach |
view |
0 |
logs |
0 |
send |
0 if data sent; 1 if session not found or dead |
stop |
0 if signal sent |
- Not a process manager. Use pm2, systemd, nohup for lifecycle management.
- Not a terminal emulator. Your terminal (Windows Terminal, iTerm, etc.) does the rendering.
- Not a window manager. No splits, tabs, panes. One PTY per session.
- Not a config-driven tool. Everything via CLI flags and env vars.
| Platform | PTY backend | Status |
|---|---|---|
| Windows 10+ | ConPTY | ✅ Primary |
| Linux | forkpty | ✅ Supported |
| macOS | forkpty | ✅ Supported |
- Bugs & feature requests: GitHub Issues
- Source code: github.com/marcfargas/holdpty
MIT — see LICENSE.