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

# SSH and PTY Sessions

> Reach a running sandbox over standard SSH, or open a programmatic PTY session over WebSocket

There are two ways to drive an interactive shell inside a sandbox:

* **[SSH](#ssh)** — connect with `ssh`, `scp`, `sftp`, `rsync`, VS Code Remote-SSH, JetBrains Gateway, and any other tool that speaks SSH. Use this when you want a normal terminal, file transfer, or port forwarding.
* **[PTY sessions](#pty-sessions)** — create a PTY over HTTPS, attach to it over a WebSocket, and drive terminal I/O programmatically. Use this when you're building a UI or browser app that needs a shell, when you need WebSocket-only access, or when you want a session you can disconnect and reattach by token.

## SSH

The Tensorlake sandbox proxy exposes a standard SSH endpoint at `sandbox.tensorlake.ai`. The username is the sandbox id (or name); your laptop's SSH key, registered once with your Tensorlake account, authenticates the connection.

### One-time setup

```bash theme={null}
tl login                                          # if you aren't already logged in
tl ssh-keys add --name laptop ~/.ssh/id_ed25519.pub
tl ssh-keys ls
```

The key is associated with your user across every project you're a member of — there's no per-sandbox or per-project re-registration.

### Connect

```bash theme={null}
ssh <sandbox-id>@sandbox.tensorlake.ai
```

You land in `/home/tl-user` as the `tl-user` POSIX account, which is in the `sudo` group. The sandbox's hostname inside the session is `tl-sbx`.

To target a specific port (default is the SSH server on 22), prefix the username with the port:

```bash theme={null}
ssh 8080-<sandbox-id>@sandbox.tensorlake.ai
```

### File transfer

`scp`, `sftp`, and `rsync` ride the same connection:

```bash theme={null}
# Push a file in
scp ./script.py <sandbox-id>@sandbox.tensorlake.ai:/workspace/

# Pull a directory out
scp -r <sandbox-id>@sandbox.tensorlake.ai:/workspace/results ./

# Interactive sftp browser
sftp <sandbox-id>@sandbox.tensorlake.ai

# Mirror with rsync
rsync -avz ./src/ <sandbox-id>@sandbox.tensorlake.ai:/workspace/src/
```

### Port forwarding

All four standard forwarding modes are supported — TCP and UNIX-socket, each direction.

**Local forward (`-L`)** — reach a service running inside the sandbox from your laptop:

```bash theme={null}
# Web server on :8000 inside the sandbox → localhost:8888 on your laptop
ssh -L 8888:localhost:8000 <sandbox-id>@sandbox.tensorlake.ai
```

**Dynamic SOCKS (`-D`)** — route arbitrary traffic through the sandbox's network namespace:

```bash theme={null}
ssh -D 1080 -N -f <sandbox-id>@sandbox.tensorlake.ai
curl --socks5 localhost:1080 https://example.com
```

**Remote forward (`-R`)** — let processes inside the sandbox reach a service running on your laptop:

```bash theme={null}
# Service on your laptop's :9000 → reachable from inside the sandbox at localhost:9000
ssh -R 9000:localhost:9000 <sandbox-id>@sandbox.tensorlake.ai
```

**UNIX-socket forwards** — same shapes with socket paths instead of ports:

```bash theme={null}
ssh -L /tmp/local.sock:/tmp/remote.sock <sandbox-id>@sandbox.tensorlake.ai
ssh -R /tmp/remote.sock:/tmp/local.sock <sandbox-id>@sandbox.tensorlake.ai
```

### VS Code Remote-SSH

Add an entry to `~/.ssh/config`:

```sshconfig theme={null}
Host my-sandbox
  HostName sandbox.tensorlake.ai
  User <sandbox-id>
  IdentityFile ~/.ssh/id_ed25519
  IdentitiesOnly yes
```

Then run **Remote-SSH: Connect to Host…** in VS Code and pick `my-sandbox`. VS Code installs its server inside the sandbox automatically. JetBrains Gateway, Cursor, and any other Remote-SSH client work the same way.

### Persistent shells

`tmux` and `screen` work normally inside the sandbox — useful if you want a session that survives an `ssh` disconnect:

```bash theme={null}
ssh <sandbox-id>@sandbox.tensorlake.ai
tmux new -s work
# … run things …
# detach with Ctrl-b d, exit ssh, reconnect later, then:
ssh <sandbox-id>@sandbox.tensorlake.ai
tmux attach -t work
```

### Troubleshooting

When auth fails, the proxy disconnects with one of three specific messages.

**Key not registered.**

```text theme={null}
your SSH public key is not registered with Tensorlake. Run `tl login` and `tl ssh-keys add ~/.ssh/id_ed25519.pub`.
```

The offered key isn't on your Tensorlake account. Run `tl ssh-keys add ~/.ssh/id_ed25519.pub`.

**Sandbox not in any of your projects.**

```text theme={null}
sandbox <id> is not present in any of your projects (verify the id with `tl sbx ls -r`).
```

Either a typo in the id, or the sandbox lives in a project you're not a member of. Run `tl sbx ls -r` to see running sandboxes in your active project.

**Sandbox is not running.**

```text theme={null}
sandbox <id> is currently <status> — resume it (`tl sbx resume <id>`) or create a new one.
```

The sandbox exists in your project but isn't `running`. For named sandboxes, `tl sbx resume <id>`; otherwise create a fresh one.

If your client offers multiple keys and one is unregistered, you'll see the static banner followed by `Permission denied (publickey).` because OpenSSH iterates through them. Constrain it to the registered key:

```sshconfig theme={null}
Host *.tensorlake.ai
  IdentitiesOnly yes
  IdentityFile ~/.ssh/id_ed25519
```

### CLI shortcut

If you don't need standard `ssh` semantics — e.g. you just want a quick shell without setting up keys — `tl sbx ssh` opens an interactive PTY using the WebSocket flow described below:

```bash theme={null}
tl sbx ssh my-sandbox
tl sbx ssh my-sandbox --shell /bin/sh
```

`tl sbx ssh` requires an interactive terminal and doesn't support port forwarding or file transfer — use `ssh`, `scp`, etc. for that.

## PTY sessions

Use PTY sessions when you need to drive an interactive shell programmatically — for example a browser-based terminal UI, a recorder, or a remote-control tool — without a real SSH client. The session is created over HTTPS and terminal I/O moves over a WebSocket.

<Note>
  The PTY management endpoints live on the sandbox proxy host, not `https://api.tensorlake.ai`:

  `https://<sandbox-id-or-name>.sandbox.tensorlake.ai`

  Create, list, get, resize, and kill requests require `Authorization: Bearer $TENSORLAKE_API_KEY`. The WebSocket attach step also requires the per-session PTY token returned from session creation.
</Note>

### Happy Path

1. Call `createPty()` or `create_pty()` on a connected sandbox client.
2. Tensorlake creates the PTY session, opens the WebSocket, and sends the initial `READY` frame for you.
3. Use the returned handle to send input, resize the terminal, stream output, wait for exit, disconnect, reconnect, or kill the session.
4. If you need to reattach later, call `connectPty()` or `connect_pty()` with the original `sessionId` and `token`.

### High-Level SDK API

The connected sandbox client now exposes a high-level PTY handle instead of making you manage WebSocket framing yourself.

The handle exposes:

* `sendInput()` / `send_input()` to write terminal input
* `resize()` to change rows and columns
* `wait()` to block until the PTY exits and get the exit code
* `disconnect()` to close the current WebSocket without killing the PTY
* `connect()` to reattach the same handle later
* `kill()` to terminate the PTY session over HTTP
* `onData()` / `on_data()` and `onExit()` / `on_exit()` to subscribe to output and exit events

<Tabs>
  <Tab title="CLI">
    Use `tl sbx ssh` when you want an interactive terminal immediately and do not need to manage PTY sessions programmatically:

    ```bash theme={null}
    tl sbx ssh my-sandbox
    ```

    ```bash theme={null}
    tl sbx ssh my-sandbox --shell /bin/sh
    ```

    `tl sbx ssh` uses the PTY API under the hood and requires an interactive terminal. For reconnectable sessions or application-managed PTY control, use the Python or TypeScript SDK.
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    from tensorlake.sandbox import Sandbox

    sandbox_client = Sandbox.create()
    try:
        pty = sandbox_client.create_pty(
            command="/bin/bash",
            args=["-l"],
            env={"TERM": "xterm-256color"},
            working_dir="/workspace",
            cols=80,
            rows=24,
        )

        pty.on_data(lambda data: print(data.decode("utf-8"), end=""))
        pty.on_exit(lambda code: print(f"\nExited: {code}"))

        pty.send_input("printf 'hello from PTY\\n'; pwd\\n")
        pty.resize(120, 40)
        pty.send_input("exit\n")

        exit_code = pty.wait()
        print(f"Final exit code: {exit_code}")
    finally:
        sandbox_client.terminate()
    ```

    To reconnect later:

    ```python theme={null}
    pty = sandbox_client.connect_pty(session_id, token)
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import { Sandbox } from "tensorlake";

    const sandboxClient = await Sandbox.create();

    try {
      const pty = await sandboxClient.createPty({
        command: "/bin/bash",
        args: ["-l"],
        env: { TERM: "xterm-256color" },
        workingDir: "/workspace",
        cols: 80,
        rows: 24,
        onData: (data) => process.stdout.write(Buffer.from(data)),
        onExit: (exitCode) => console.log("Exited:", exitCode),
      });

      await pty.sendInput("printf 'hello from PTY\\n'; pwd\\n");
      await pty.resize(120, 40);
      await pty.sendInput("exit\\n");

      const exitCode = await pty.wait();
      console.log("Final exit code:", exitCode);
    } finally {
      await sandboxClient.terminate();
    }
    ```

    To reconnect later, keep `pty.sessionId` and `pty.token` and call:

    ```typescript theme={null}
    const pty = await sandboxClient.connectPty(sessionId, token, {
      onData: (data) => process.stdout.write(Buffer.from(data)),
    });
    ```
  </Tab>
</Tabs>

<Note>
  `createPty()` / `create_pty()` already open the WebSocket and send `READY`. Use `connectPty()` / `connect_pty()` only when you are reattaching to an existing session.
</Note>

### Disconnect or kill

`disconnect()` closes the WebSocket but leaves the PTY running, so you can reattach later with `connectPty()` / `connect_pty()`. `kill()` terminates the session over HTTP.

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    # Detach without killing the shell — reconnect later with sandbox_client.connect_pty(...)
    pty.disconnect()

    # Terminate the session immediately
    pty.kill()
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    // Detach without killing the shell — reconnect later with sandboxClient.connectPty(...)
    pty.disconnect();

    // Terminate the session immediately
    await pty.kill();
    ```
  </Tab>
</Tabs>

### Raw HTTP and WebSocket Flow

The raw protocol is small enough that you can drive it yourself from any HTTP client plus any WebSocket client. These calls assume you already have a running sandbox ID or sandbox name.

#### 1. Create the PTY session

```bash theme={null}
curl -sS -X POST https://<sandbox-id>.sandbox.tensorlake.ai/api/v1/pty \
  -H "Authorization: Bearer $TENSORLAKE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "command": "/bin/bash",
    "args": ["-l"],
    "env": {"TERM": "xterm-256color"},
    "working_dir": "/workspace",
    "rows": 24,
    "cols": 80
  }'
```

Response:

```json theme={null}
{
  "session_id": "LYtJOrxE9Kz3bphPUDzuX",
  "token": "<pty-session-token>"
}
```

#### 2. Attach the WebSocket

Open this URL:

```text theme={null}
wss://<sandbox-id>.sandbox.tensorlake.ai/api/v1/pty/<session-id>/ws
```

Send the PTY token on the upgrade request:

```http theme={null}
X-PTY-Token: <pty-session-token>
```

If your client cannot set headers, append `?token=<pty-session-token>` to the WebSocket URL instead.

#### 3. Exchange PTY frames

| Direction        | Bytes                               | Meaning                                                      |
| ---------------- | ----------------------------------- | ------------------------------------------------------------ |
| Client -> server | `02`                                | `READY`: flush any buffered output                           |
| Client -> server | `00` + UTF-8 bytes                  | Send terminal input                                          |
| Client -> server | `01` + `cols` + `rows`              | Resize terminal, with `cols` then `rows` as big-endian `u16` |
| Server -> client | `00` + raw bytes                    | Terminal output                                              |
| Server -> client | `03` + 4-byte big-endian signed int | Process exit code                                            |

Common examples:

| Action           | Bytes               |
| ---------------- | ------------------- |
| Send `READY`     | `02`                |
| Run `pwd\n`      | `00 70 77 64 0a`    |
| Run `exit\n`     | `00 65 78 69 74 0a` |
| Exit code `0`    | `03 00 00 00 00`    |
| Resize to 120x40 | `01 00 78 00 28`    |

#### 4. Close or abort

To close cleanly, write `exit\n` to the shell and wait for the `0x03` exit frame followed by the normal WebSocket close.

To terminate the session immediately:

```bash theme={null}
curl -X DELETE https://<sandbox-id>.sandbox.tensorlake.ai/api/v1/pty/<session-id> \
  -H "Authorization: Bearer $TENSORLAKE_API_KEY"
```

### Notes

* `createPty()` / `create_pty()` send `READY` for you immediately after the socket opens.
* Closing the WebSocket does not kill the PTY session. You can reconnect while the shell is still running.
* Persist the original PTY token if you plan to reconnect. [Get PTY Session](/api-reference/v2/pty/get) and [List PTY Sessions](/api-reference/v2/pty/list) do not return it again.
* PTY sessions with no connected clients are killed after 300 seconds of inactivity.
* You can resize either with the `0x01` WebSocket frame or with [Resize PTY Session](/api-reference/v2/pty/resize).
* For the endpoint-by-endpoint API reference, see [PTY Sessions API](/api-reference/v2/pty/introduction).

## Related Guides

<CardGroup cols={2}>
  <Card title="Commands" icon="terminal" href="/sandboxes/commands">
    Run one-shot commands without an interactive shell.
  </Card>

  <Card title="Processes" icon="gears" href="/sandboxes/processes">
    Long-running background processes managed by the sandbox daemon.
  </Card>

  <Card title="Local Tunnels" icon="arrow-right-arrow-left" href="/sandboxes/tunnels">
    Forward arbitrary TCP ports (Postgres, VNC, custom binary protocols) over an authenticated WebSocket.
  </Card>

  <Card title="Lifecycle" icon="arrows-spin" href="/sandboxes/lifecycle">
    How long a sandbox lives, and what happens on suspend, resume, and terminate.
  </Card>
</CardGroup>
