> ## 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.

# PTY

> Python SDK reference for sandbox.pty — create and drive interactive pseudo-terminals inside the sandbox.

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](/features/pty).

## `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`](#ptyhandle) that can send input, resize, or kill the
session and receive its output.

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

| Name              | Type                              | Default                     | Description                                                                                                                                |
| ----------------- | --------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `size`            | `PtySize`                         | `PtySize(cols=80, rows=24)` | Initial terminal size.                                                                                                                     |
| `user`            | `str`                             | `"user"`                    | User the shell runs as.                                                                                                                    |
| `cwd`             | `str \| None`                     | `None`                      | Starting working directory.                                                                                                                |
| `envs`            | `dict[str, str] \| None`          | `None`                      | Environment variables merged into the shell env. `TERM` defaults to `xterm-256color` unless overridden here.                               |
| `timeout`         | `float \| None`                   | `3600`                      | PTY session TTL in seconds. `0` keeps the session alive until the sandbox itself expires.                                                  |
| `on_data`         | `Callable[[bytes], None] \| None` | `None`                      | If provided, a background reader thread calls this for every output chunk. Without it you consume output by iterating the returned handle. |
| `request_timeout` | `float \| None`                   | `None`                      | httpx timeout applied to the create POST only.                                                                                             |

### Returns

A [`PtyHandle`](#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.

```python theme={null}
# 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 | None` — `None` 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.

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

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

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

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

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

```python theme={null}
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`](/sdks/python/async-sandbox) 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](/cookbook/pty/interactive-terminal).
