holdpty — Detached PTY Sessions
Launch commands in a real pseudo-terminal. Attach, view, or dump output later. Cross-platform: Windows (ConPTY, named pipes) + Linux (forkpty, UDS) + macOS.
Project Location
C:\dev\holdpty
Install & Build
cd C:\dev\holdpty
npm install
npm run build
# CLI: node dist/cli.js (or 'holdpty' after npm install -g)
Core Commands
Launch a session
--fg or --bg is required (no default).
# Detached (returns immediately, prints session name to stdout)
holdpty launch --bg --name worker1 -- pi -p "analyze codebase"
# Foreground (blocks until child exits, returns child exit code)
holdpty launch --fg --name build -- make all
# Auto-generated name (format: basename-xxxx)
SESSION=$(holdpty launch --bg -- node server.js)
# Set initial PTY dimensions (default: 120x40)
holdpty launch --bg --cols 200 --rows 50 -- /bin/zsh
-- is optional. PowerShell strips -- before it reaches the process, so holdpty launch --bg node server.js works the same as holdpty launch --bg -- node server.js.
Windows .cmd wrappers do NOT work. npm-installed CLIs on Windows use .cmd shims (e.g. pi.cmd, tsc.cmd). holdpty (via node-pty) cannot execute these — the holder will fail to start. Always resolve to the actual .js entry point:
# ❌ WRONG — pi resolves to pi.cmd, holder fails
holdpty launch --bg --name agent -- pi -p "prompt"
# ✅ CORRECT — use node with the actual cli.js
holdpty launch --bg --name agent -- node "C:\path\to\cli.js" -p "prompt"
# Find the real path behind a .cmd shim:
cat "$(which pi)" | head -5 # look for the .js path
List sessions
holdpty ls # human-readable table
holdpty ls --json # machine-readable JSON array
Stale sessions (crashed holders) are auto-detected and cleaned on every ls.
Attach (interactive, single-writer)
holdpty attach worker1
# Detach: Ctrl+A then d
# Session keeps running after detach
Only one attachment at a time. Error if already attached. Requires a real TTY.
View (read-only, multiple viewers)
holdpty view worker1
Outputs real PTY data (escape sequences, TUI, colors) to stdout. Includes both buffer replay (history) and live stream. Multiple viewers allowed simultaneously.
Logs (dump buffer, exit — or follow live)
holdpty logs worker1 # full buffer, exit
holdpty logs worker1 --tail 50 # last 50 lines, exit
holdpty logs worker1 -f # full buffer + live stream
holdpty logs worker1 -f --tail 50 # last 50 lines + live stream (tail -f)
holdpty logs worker1 -f --no-replay # live output only, skip history
Send (non-exclusive input injection)
# Send text to a session without attaching (like tmux send-keys)
holdpty send worker1 "npm test"
# Pipe from stdin
echo "exit" | holdpty send worker1 --stdin
Multiple senders can run concurrently — no exclusive lock. Works alongside attached clients and viewers. No ordering guarantee between concurrent senders.
Stop a session
holdpty stop worker1
Kills both the child process and the holder process.
Info
holdpty info worker1 # JSON with name, pid, childPid, command, cols, rows, startedAt, active
Stdout Discipline
This matters for scripting and agent use:
| Command | stdout | stderr |
|---|---|---|
send |
Nothing | Errors only |
launch --bg |
Session name only | Nothing |
launch --fg |
Session name | Nothing |
view |
PTY data only | Status messages |
logs |
PTY data only | Status messages |
ls |
Session list | "No active sessions" if empty |
ls --json |
JSON array | Nothing |
info |
JSON object | Errors only |
attach |
Terminal takeover | N/A |
stop |
Nothing | Confirmation |
Exit Codes
| Command | Exit code |
|---|---|
launch --bg |
0 on success |
launch --fg |
Child's exit code |
attach |
Child's exit code (if child exits) or 0 (on detach) |
logs |
0 |
view |
0 |
send |
0 if sent; 1 if session not found or dead |
stop |
0 |
| Any error | 1 |
Environment Variables
| Variable | Purpose | Default |
|---|---|---|
HOLDPTY_DIR |
Session metadata directory | %TEMP%\dt\ (Win) / $XDG_RUNTIME_DIR/dt/ (Linux) |
HOLDPTY_DETACH |
Custom detach sequence (hex) | 0x01,0x64 (Ctrl+A then d) |
HOLDPTY_LINGER_MS |
Shutdown linger time in ms | 5000 |
Platform Details
| Windows | Linux/macOS | |
|---|---|---|
| PTY backend | ConPTY | forkpty |
| IPC transport | Named pipes (//./pipe/holdpty-<hash>-<name>) |
Unix domain sockets ({dir}/{name}.sock) |
| Signal behavior | SIGTERM = instant TerminateProcess |
SIGTERM = graceful |
| Metadata | %TEMP%\dt\{name}.json |
$XDG_RUNTIME_DIR/dt/{name}.json |
Named pipes include a hash of HOLDPTY_DIR to isolate different environments.
Common Agent Patterns
Launch an agent and check on it later
SESSION=$(holdpty launch --bg --name analysis -- node.exe agent.js)
# ... later ...
holdpty logs "$SESSION" | tail -20
Supervise a running agent
holdpty view worker1
# Watch in real-time, read-only, Ctrl+C to stop viewing
Check if a session is alive
holdpty info worker1 # JSON with "active": true/false
holdpty ls --json # all sessions as array
Scripting pattern
NAME=$(holdpty launch --bg --name myagent -- node.exe server.js)
echo "Launched: $NAME"
sleep 2
holdpty logs "$NAME" | grep "listening"
holdpty stop "$NAME"
Architecture (for developers)
- One holder process per session (no central daemon)
- Binary length-prefixed protocol:
[1B type][4B len BE][payload](8 message types) - 1MB ring buffer for output replay
- Filesystem is the registry (
.jsonmetadata files) - Raw byte relay (no terminal state machine / ANSI parsing)
Source: src/ — cli.ts, holder.ts, client.ts, protocol.ts, ring-buffer.ts, session.ts, platform.ts
What holdpty is NOT
- Not a process manager → use pm2 / systemd / nohup for lifecycle
- Not a terminal emulator → your terminal renders the output
- Not tmux/screen → no splits, tabs, config files, status bars
Converted and distributed by TomeVault — claim your Tome and manage your conversions.