Skip to main content
This cookbook walks through a small script that attaches your local terminal to a sandbox PTY: every keystroke you type is forwarded to the remote shell, every byte the shell emits streams back to your screen, and the pseudo-terminal resizes when you drag your window. It’s the same idea as ssh into a VM, but the VM is a fresh declaw sandbox created on demand.

What you’ll learn

  • Putting your local TTY in raw mode so keystrokes flow without buffering
  • Installing a SIGWINCH handler that propagates window resizes to the sandbox
  • Writing output bytes to local stdout via the on_data callback
  • Detecting Ctrl-D locally and sending a clean exit\n

Prerequisites

export DECLAW_API_KEY="your-api-key"
export DECLAW_DOMAIN="your-declaw-instance.example.com:8080"
Also install the SDK if you haven’t:
pip install declaw

Code

import fcntl
import os
import signal
import struct
import sys
import termios
import time
import tty

from declaw import Sandbox
from declaw.sandbox.commands.models import PtySize


def term_size() -> PtySize:
    """Current cols/rows of the local TTY."""
    try:
        cr = struct.unpack(
            "hh", fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, b"....")
        )
        return PtySize(cols=cr[1], rows=cr[0])
    except Exception:
        return PtySize(cols=120, rows=30)


def main() -> int:
    if not sys.stdin.isatty():
        print("stdin is not a TTY — run this in a real terminal", file=sys.stderr)
        return 2

    with Sandbox.create() as sbx:
        print(f"[declaw] sandbox: {sbx.sandbox_id} — Ctrl-D to quit\n", flush=True)

        handle = sbx.pty.create(
            size=term_size(),
            on_data=lambda b: (sys.stdout.buffer.write(b), sys.stdout.buffer.flush()),
            timeout=3600,
        )

        # Propagate local window resizes to the sandbox via SIGWINCH.
        signal.signal(signal.SIGWINCH, lambda *_: handle.resize(term_size()))

        fd = sys.stdin.fileno()
        old_attrs = termios.tcgetattr(fd)
        try:
            tty.setraw(fd)
            time.sleep(0.1)  # let the PTY print its first prompt
            while True:
                data = os.read(fd, 1024)
                if not data:
                    break
                if data == b"\x04":          # Ctrl-D → graceful remote exit
                    handle.send_stdin("exit\n")
                    break
                handle.send_stdin(data)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_attrs)

        result = handle.wait(timeout=5)
        print(f"\r\n[declaw] pty exited: {result.exit_code}", flush=True)
    return 0


if __name__ == "__main__":
    sys.exit(main())

Running it

Drop the snippet into pty_live.py and run it in an interactive terminal (iTerm2, Alacritty, Kitty, Terminal.app, tmux — any real TTY works):
export DECLAW_API_KEY="your-api-key"
export DECLAW_DOMAIN="api.declaw.ai"
python pty_live.py
You land at a bash prompt inside a fresh sandbox. Try:
  • ls /etc/os-release — reads a file
  • htop — full-screen TUI, quit with q
  • vim /tmp/x.txt — editor loads, :wq to save
  • stty size — prints cols rows matching your local terminal
  • Resize your window — stty size updates on the next probe
  • Ctrl-D — closes the session cleanly

How it works

  1. Sandbox.create() — new microVM, 300s default lifetime.
  2. sbx.pty.create(on_data=...) — spawns a real bash -l inside the VM, opens an SSE stream, and invokes on_data with every chunk.
  3. Raw-mode local TTYtty.setraw(fd) disables line buffering and local echo so each keystroke reaches the script immediately. The remote bash echoes for us.
  4. os.read(fd, 1024) — pulls bytes the user typed. We forward them untouched with handle.send_stdin(data).
  5. SIGWINCH handler — calls handle.resize(...) with the new local size, which fires TIOCSWINSZ inside the sandbox; htop and friends redraw for the new dimensions.
  6. Ctrl-D (EOT, \x04) — we intercept locally and send exit\n so the remote shell exits cleanly instead of sending raw EOT and confusing bash.
  7. handle.wait() — blocks until the PTY stream emits its exit frame, returns a PtyResult.

See also