> ## Documentation Index
> Fetch the complete documentation index at: https://docs.declaw.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Interactive sandbox shell in your local terminal

> Drop your local terminal into a sandbox shell — raw-mode stdin forwarding, live output streaming, cursor resize support, clean exit. The ssh -style hand-off in ~80 lines of Python.

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

<Snippet file="snippets/env-setup.mdx" />

Also install the SDK if you haven't:

```bash theme={null}
pip install declaw
```

## Code

```python theme={null}
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):

```bash theme={null}
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 sandbox, 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 TTY** — `tty.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`](/sdks/python/pty#ptyresult).

## See also

* [PTY feature overview](/features/pty) — why and when to use a PTY.
* [Python SDK: PTY reference](/sdks/python/pty) — `create`, `connect`, `PtyHandle`, `PtyResult`.
* [TypeScript SDK: PTY reference](/sdks/typescript/pty) — same flow from a Node.js CLI.

## More PTY recipes

* [REPL demo](/cookbook/pty/pty-repl) — simplest PTY round-trip
* [ANSI features](/cookbook/pty/pty-features) — colours, cursor movement, tput
* [Dashboard](/cookbook/pty/pty-dashboard) — live monitoring with sparklines
* [Visual showcase](/cookbook/pty/pty-showcase) — rainbow, gradients, Matrix rain
* [Agent with PTY](/cookbook/pty/agent-with-pty) — Anthropic agent with interactive handoff
* [OpenAI Agents PTY](/cookbook/openai-agents/pty) — PTY through the OpenAI adapter
