Skip to main content
A PTY (pseudo-terminal) attaches a real TTY to a process inside the sandbox. Unlike commands.run, which returns a single {stdout, stderr, exit_code} blob when the process finishes, a PTY lets you:
  • Stream bytes as they’re produced — the right choice for anything that paints the screen (progress bars, TUI apps, live logs).
  • Send keystrokes mid-execution — required for password prompts, OAuth pasteback, confirmation dialogs, REPLs, and editors.
  • Resize on the fly — if the user drags a terminal pane, the remote program sees a SIGWINCH and redraws.
  • Reconnect and fan out — multiple clients can subscribe to the same PTY concurrently, and clients can disconnect without killing the shell.
Inside the sandbox the PTY runs an interactive bash -l (login shell) with TERM=xterm-256color pre-set, so ANSI colour codes, cursor escapes, tput queries, and ncurses-based TUIs (vim, htop, less, nano) all render correctly.

When to use PTY vs commands.run

Use commands.run when…Use pty.create when…
Command takes input once (argv / piped stdin) and exitsCommand prompts for input mid-run
You only care about the final stdout / exit codeYou need live output as bytes arrive
Output is line-oriented plain textOutput has ANSI escapes or cursor movement
pip install, pytest, python script.py, go buildgh auth login, vim, htop, sudo, ssh
Default to commands.run. Reach for pty.create only when the command requires a terminal.

Architecture

pty.create takes four REST calls plus one SSE stream:
OperationMethod / route
Create sessionPOST /sandboxes/{id}/pty — returns the remote pid
Send stdinPOST /sandboxes/{id}/pty/{pid}/stdin
ResizePATCH /sandboxes/{id}/pty/{pid}
KillDELETE /sandboxes/{id}/pty/{pid}
Live outputGET /sandboxes/{id}/pty/{pid}/stream (Server-Sent Events, base64-encoded frames)
The SSE stream stays open for the life of the session. Output bytes are emitted as event: data frames with base64-encoded payload, and a final event: exit frame announces the remote exit code.

Session lifecycle

A PTY session is bounded by two independent timeouts — whichever fires first ends the session:
  • The sandbox timeout (set at Sandbox.create(timeout=...), default 300s) kills the whole microVM and every PTY inside it.
  • The PTY timeout (set at sandbox.pty.create(timeout=...), default 3600s) kills just that one PTY. Pass 0 for no PTY-level TTL — sessions then live until the sandbox itself expires.
For an interactive coding-agent session you typically raise both:
sbx = Sandbox.create(timeout=3600)          # VM lives an hour
handle = sbx.pty.create(timeout=0)          # PTY lives as long as VM

Quick start

Callback-style — your function receives every chunk of PTY output as it arrives. Good for forwarding to an xterm.js instance or your local terminal.
import sys
from declaw import Sandbox
from declaw.sandbox.commands.models import PtySize

with Sandbox.create() as sbx:
    handle = sbx.pty.create(
        size=PtySize(cols=120, rows=30),
        on_data=lambda chunk: sys.stdout.buffer.write(chunk),
        timeout=3600,
    )
    handle.send_stdin("echo hello && exit\n")
    result = handle.wait(timeout=10)
    print(f"\nexit: {result.exit_code}")

Next steps