About pty-bridge
pty-bridge is a CLI for managing interactive terminal sessions with full PTY support, for users and agents handling commands that need a real terminal. It supports session input, output reading, execution, and ownership controls across macOS, Linux, and Windows.
Key features
- 01Starts and manages PTY sessions
- 02Reads terminal output incrementally or by range
- 03Sends text and special keys
- 04Runs commands and waits for completion
- 05Tracks session ownership
Use cases
- 01Interact with SSH sessions
- 02Operate REPLs and database CLIs
- 03Handle password prompts and TUI apps
- 04Monitor command and service output
MCP support
The input describes pty-bridge as a CLI and skill for managing terminal sessions; it does not specify an MCP interface.
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ briqt/ pty-bridge --skill pty-bridgeAfter installing, type/pty-bridgein Claude Code to confirm it loaded.
Details
From the project READMEpty-bridge
pty-bridge is a CLI that manages interactive terminal sessions with full PTY support. Use it when the standard Bash tool can't handle interactive programs — SSH, REPLs, database CLIs, TUI apps, or any command that expects a real terminal.
This manual requires pty-bridge 1.5.0+. Skill files and the CLI are installed separately:
pty-bridge --version
pty-bridge doctor --json # inspects an existing daemon; never starts one
npm i -g github:briqt/pty-bridge --install-links
npx skills add briqt/pty-bridge -g -y
The doctor reports CLI/Node paths, native PTY availability, the socket and the running daemon's version. Installing a new CLI does not replace an existing daemon or its sessions. A legacy daemon without a version field needs to drain/exit before new runtime features apply.
Setup
If pty-bridge is not available, install it from GitHub (not the npm registry):
npm i -g github:briqt/pty-bridge --install-links
Requirements: Node.js 18+. Supported on macOS, Linux, and Windows.
When to Use
- SSH into remote servers
- Interactive REPLs (python, node, irb, psql, mysql, etc.)
- Programs that prompt for passwords or confirmations
- TUI applications (htop, vim, less, etc.)
- Any command that hangs or misbehaves with the regular Bash tool
Isolation (required)
All agents on the same OS user share one daemon. Never pick a session from list unless the user asked you to take it over.
- Set a stable owner for this conversation, then pass it on every mutating call (tool calls may use fresh shells):
export PTY_BRIDGE_OWNER="agent-$(uuidgen | cut -c1-8)" pty-bridge start ssh user@host --keepalive 30 --owner "$PTY_BRIDGE_OWNER" --json # Store sessionId and owner from the JSON response. pty-bridge exec <id> "pwd" --owner <stored-owner> --wait-exit - Use only the
sessionId(--json) orSession: <id>returned by yourstart. Store both the id and owner. - Mutating another owner's session (
write/exec/sendkey/kill/resize) is rejected. Use--forceonly when the user explicitly wants to take over. listshowsowner=so you can see which sessions are yours.
Commands
pty-bridge [--timeout <ms>] <command> [options]
pty-bridge start <command> [args...] # Start a PTY session
pty-bridge read <id> [options] # Read output (see Read options)
pty-bridge write <id> <input> # Send input (or pipe via stdin)
pty-bridge exec <id> <command> [options] # Execute command and return new output
pty-bridge sendkey <id> <key> # Send special key
pty-bridge wait-for <id> <pattern> [--timeout <s>] # Block until pattern appears (default: 30s)
pty-bridge snapshot <id> # Capture current visible screen
pty-bridge list # Active sessions + lost-session tombstones
pty-bridge kill <id> # Terminate a session
pty-bridge resize <id> <cols> <rows> # Resize terminal (sends SIGWINCH to the child)
pty-bridge status # Daemon PID, memory, lost sessions
Global Options
pty-bridge --timeout 60000 exec <id> "slow-command" # Client RPC timeout in ms (must be BEFORE the subcommand)
Use --rpc-timeout-ms for the RPC budget (before or after a subcommand); legacy
--timeout still works, but wait-for --timeout means seconds. Use exec --wait
for the command wait budget. RPC timeout auto-extends to max(30000, waitMs+5000).
read --help, exec --help, and other subcommand help calls are supported.
Start Options
pty-bridge start ssh user@host --keepalive 30 --owner "$PTY_BRIDGE_OWNER"
pty-bridge start cmd --wait 1000
pty-bridge start cmd --cols 200 --rows 50
Read Options
Unknown flags error out (they are not swallowed). --full, --from, --from-exec, and --last/--lines are mutually exclusive.
pty-bridge read <id> # Incremental new output (advances cursor)
pty-bridge read <id> --full # Entire rendered buffer (does not advance cursor)
pty-bridge read <id> --from 12:40 # Rendered line 12, UTF-8 byte column 40
pty-bridge read <id> --from 12 # From line 12 (resume cursor from exec metadata)
pty-bridge read <id> --from-exec # From last exec start (after an RPC timeout)
pty-bridge read <id> --last 60 # Last 60 lines
pty-bridge read <id> --lines 60 # Alias of --last
pty-bridge read <id> --raw [--full] # Unwrapped PTY bytes (no xterm line wrapping)
pty-bridge read <id> --max-bytes 100000 # Truncate; 0 = unlimited (default 256000)
pty-bridge read <id> --buffer normal # normal | alternate | active (default)
snapshot is the current visible screen (cols×rows), not a log tail. Empty-looking snapshots usually mean a clear/alternate screen — use read --last or read --raw.
Exec Options
pty-bridge exec <id> "ls -la" # wait 200ms, then return output so far --owner "$PTY_BRIDGE_OWNER"
pty-bridge exec <id> "make build" --wait-for-idle 500 # poll until output idle (max 5s unless --wait) --owner "$PTY_BRIDGE_OWNER"
pty-bridge exec <id> "make build" --wait-exit --wait 120000 # shell only: wait until command exits --owner "$PTY_BRIDGE_OWNER"
pty-bridge exec <id> "dmesg" --max-bytes 100000 --owner "$PTY_BRIDGE_OWNER"
--wait-exit appends a sentinel (cmd; echo PTYBRIDGE_DONE_<token>:$?). Use it only in a shell (bash/ssh). Do not use it inside python/mysql/vim.
Wait-for Options
pty-bridge wait-for <id> "Uvicorn running" --timeout 120
pty-bridge wait-for <id> "Started" --from-now --timeout 60 # ignore history
pty-bridge wait-for <id> "Started" --from 0 # full buffer (legacy)
Default search window is the last 200 lines plus new output, so an old Started from hours ago will not match.
Special Keys
enter, tab, escape, space, backspace, delete, up, down, left, right, home, end, pageup, pagedown, ctrl-a through ctrl-z, ctrl-\, ctrl-]
Workflow Patterns
SSH Session (recommended)
export PTY_BRIDGE_OWNER="agent-<unique-conversation-id>"
# Retain this literal owner between tool calls, and pass it again each time.
pty-bridge start ssh user@host --keepalive 30 --owner "$PTY_BRIDGE_OWNER"
# If password / MFA prompt:
echo -n "password" | pty-bridge write <id> --stdin --owner "$PTY_BRIDGE_OWNER"
pty-bridge sendkey <id> enter --owner "$PTY_BRIDGE_OWNER"
pty-bridge read <id>
# Prefer --wait-exit for shell commands so you get done/failed + exitCode:
pty-bridge exec <id> "ls -la" --wait-exit --wait 15000 --owner "$PTY_BRIDGE_OWNER"
pty-bridge exec <id> "apt update" --wait-exit --wait 120000 --owner "$PTY_BRIDGE_OWNER"
pty-bridge exec <id> "exit" --wait-exit --wait 5000 --owner "$PTY_BRIDGE_OWNER"
Wait for Service Startup
pty-bridge exec <id> "docker compose up -d" --wait-exit --wait 60000 --owner "$PTY_BRIDGE_OWNER"
pty-bridge exec <id> "docker logs -f myservice" --wait 1000 --owner "$PTY_BRIDGE_OWNER"
pty-bridge wait-for <id> "Uvicorn running" --from-now --timeout 120
Interactive REPL
pty-bridge start python3
pty-bridge exec <id> "print('hello')" # no --wait-exit (not a shell) --owner "$PTY_BRIDGE_OWNER"
pty-bridge sendkey <id> ctrl-d
Output Format Convention
- stdout: PTY content
- stderr: one metadata line
[key=value ...], e.g.[status=still_running cursor=42 startCursor=10 alive=true truncated=false totalBytes=1200 returnedBytes=1200] statusisdone|still_running|failed.still_runningis not a failure — the command was accepted. Do not resend.cursoris the next--fromvalue: a rendered line orline:byte-column; with--rawit is an absolute UTF-8 byte offset, stable across buffer eviction.- Raw
lostBytes>0means old output was evicted; report the gap instead of treating the output as complete. A bounded read never advances past bytes actually returned. - If
truncated=true, continue withread <id> --from <cursor>(or--from-exec). list/statusmay printWarning: daemon restarted at <iso>, N session(s) lost. Those PTYs are gone; remotenohupjobs may still be running. Re-login; do not guess another agent's session fromlist.
Error Recovery
| Symptom | Cause | Action |
|---|---|---|
command not found: pty-bridge | Not installed | npm i -g github:briqt/pty-bridge --install-links |
connect ENOENT or daemon start error | Daemon died | pty-bridge status; next start respawns it. Check list for lost tombstones. |
Session not found + lost warning | Daemon restarted and dropped in-memory sessions | Re-start. Do not reuse the old id. |
Session ... is owned by | Owner mismatch | Use your own id, or --force only if asked to take over |
RPC timeout / status=still_running | Wait budget or RPC limit hit; command already sent | Do not exec again. read <id> --from-exec or wait-for |
| SSH password prompt | Interactive auth | write --stdin then sendkey enter |
wait-for timeout | Pattern not in last 200 lines + new output | --from-now / --from 0; check the returned output |
Cannot find module @lydell/node-pty-… | No prebuilt binary | linux/darwin/win32 × x64/arm64. Reinstall with --install-links |
Output wrapped (S\nYS) | xterm render at current cols | read --raw (do not resize if you need the remote TTY unchanged) |
| Unknown option | Typo / unsupported flag | Flags are not ignored; fix the flag name |
Important Notes
- Prefer
--wait-exitin shells over guessing--wait. For REPLs/TUIs, useexec/writewithout--wait-exit. readis incremental by default.--full/--from/--lastdo not advance the incremental cursor.execdoes not advance thereadcursor.- Timeout ≠ failed. Never resend an
execbecause ofstill_runningor RPC timeout. writesends text as-is — usesendkey enterafterward (orexec/--wait-exit).- Secrets:
echo -n "password" | pty-bridge write <id> --stdin --owner "$PTY_BRIDGE_OWNER" killyour sessions when done. Daemon auto-exits after 5 minutes with no alive sessions (graceful exit clears tombstones).ctrl-cvia sendkey interrupts stuck commands.- Terminal defaults to 120x40.
resizedoes change the child's TTY size. - Prefer
--rpc-timeout-msto disambiguate RPC milliseconds fromwait-for --timeoutseconds. - Default read/exec payload cap is 256000 bytes; metadata tells you if more remains.
Machine-readable sessions and filtering
pty-bridge start bash --owner <conversation-owner> --json
pty-bridge list --owner <conversation-owner> --alive-only --json
pty-bridge exec <id> "pwd" --owner <conversation-owner> --wait-exit
pty-bridge kill <id> --owner <conversation-owner>
A filtered list does not authorize taking over any session. Retain the id and owner
from your own start response. snapshot shows a screen; use read --last 30 for a log tail.
For scripts, large exports, or file transfer over SSH, prefer a file/SFTP channel over
base64 in a PTY. RPC timeout means a submitted command may still be running: resume
with a read rather than submitting it again.