Skip to main content
The Python SDK exposes PTY support through sandbox.pty. The module has three callables you’ll use directly: Pty.create() to start a new session, Pty.connect() to reattach to an existing one, and the low-level Pty.send_stdin / resize / kill trio when you already hold a pid. For conceptual background see the PTY feature overview.

sandbox.pty.create(...)PtyHandle

Create a new PTY session. The sandbox spawns an interactive bash -l login shell with TERM=xterm-256color pre-set and returns a PtyHandle that can send input, resize, or kill the session and receive its output.
handle = sandbox.pty.create(
    size=PtySize(cols=120, rows=30),    # terminal dimensions
    user="user",                        # shell user (defaults to "user")
    cwd="/workspace",                   # starting directory (optional)
    envs={"FOO": "bar"},                # extra env vars (merged into shell env)
    timeout=3600,                       # PTY TTL in seconds; 0 = indefinite
    on_data=lambda b: stdout.write(b),  # callback for live output (optional)
    request_timeout=None,               # httpx timeout on the POST itself
)

Parameters

NameTypeDefaultDescription
sizePtySizePtySize(cols=80, rows=24)Initial terminal size.
userstr"user"User the shell runs as.
cwdstr | NoneNoneStarting working directory.
envsdict[str, str] | NoneNoneEnvironment variables merged into the shell env. TERM defaults to xterm-256color unless overridden here.
timeoutfloat | None3600PTY session TTL in seconds. 0 keeps the session alive until the sandbox itself expires.
on_dataCallable[[bytes], None] | NoneNoneIf provided, a background reader thread calls this for every output chunk. Without it you consume output by iterating the returned handle.
request_timeoutfloat | NoneNonehttpx timeout applied to the create POST only.

Returns

A PtyHandle.

sandbox.pty.connect(pid, on_data=None)PtyHandle

Reattach to a PTY session that’s already running. Useful when another process created the session (e.g. a background worker) or when you want multiple UI clients to share the same shell.
# process A — owns the shell, prints the pid somewhere durable
handle = sandbox.pty.create(...)
print(handle.pid)

# process B — later, elsewhere
handle = sandbox.pty.connect(pid, on_data=my_forwarder)
Multiple subscribers see output from the moment they connect — there’s no scrollback replay. If the remote process has already exited, the stream immediately emits the cached exit frame.

PtyHandle

Returned by both create() and connect(). Exposes the full session lifecycle.

Properties

  • handle.pid: int — remote process id (matches what you’d see in ps inside the sandbox).
  • handle.exit_code: int | NoneNone while the session is live; becomes the remote exit code once the stream terminates.

Methods

handle.send_stdin(data: bytes | str, request_timeout=None) -> None

Forward keystrokes / text to the shell. Accepts bytes or str; str is UTF-8 encoded.
handle.send_stdin("echo hello\n")
handle.send_stdin(b"\x03")  # Ctrl-C

handle.resize(size: PtySize, request_timeout=None) -> None

Change the remote terminal dimensions. Equivalent to TIOCSWINSZ — fires SIGWINCH inside, so ncurses apps like vim and htop redraw.
handle.resize(PtySize(cols=160, rows=50))

handle.disconnect() -> None

Stop consuming the output stream without killing the remote process. The PTY keeps running server-side; a subsequent sandbox.pty.connect(pid) reattaches a new callback. Use this to pause/resume a UI or hand the session off to another client.

handle.kill(request_timeout=None) -> bool

Terminate the remote shell (SIGKILL to the process group). Returns True if the session existed at the time of the call. Idempotent.

handle.wait(timeout: float | None = None) -> PtyResult

Block until the remote shell exits and return a PtyResult. If the handle was created with an on_data callback, this joins the background reader thread. Otherwise it drains the stream inline, discarding bytes — iterate the handle directly if you care about the output.

Iterator

PtyHandle is iterable — each iteration yields the next chunk of output as bytes:
handle = sandbox.pty.create(size=PtySize(100, 30))
handle.send_stdin("ls -la && exit\n")
for chunk in handle:
    sys.stdout.buffer.write(chunk)
Use the iterator or on_data, not both — they consume the same stream.

PtyResult

The value returned from handle.wait().
@dataclass
class PtyResult:
    exit_code: int
Int-coercible — int(result) and result == 0 both work so existing code that treated it as a plain integer keeps compiling.

Low-level API (by pid)

When you don’t hold a PtyHandle — for example you’re wiring an HTTP route where the client passes pid as a query param — use the module-level operations:
sandbox.pty.send_stdin(pid, data: bytes | str)
sandbox.pty.resize(pid, size: PtySize)
sandbox.pty.kill(pid) -> bool
All three hit the same REST endpoints as the PtyHandle methods; they just don’t require you to keep the handle around.

PtySize

from declaw.sandbox.commands.models import PtySize

PtySize(cols=120, rows=30)
Both fields default to 80 × 24 when the dataclass is constructed with no args.

Threading notes

  • The on_data callback runs on a background daemon thread. Don’t do long blocking work inside it — write bytes to a queue and process them elsewhere if needed.
  • send_stdin, resize, kill, and disconnect are safe to call from any thread.
  • wait() is synchronous. If you need an async loop, use the AsyncSandbox PTY module instead.

Example: hand off a PTY to your local terminal

A full “ssh-style” forwarder where keystrokes go to the sandbox and output comes back to your terminal, with SIGWINCH + raw-mode + clean exit is in the interactive terminal cookbook.