# Remote Hands

Keep inference, subagents, and durability in one brain process, and run Code Mode cells and tools on hands such as laptops, build machines, or sandboxes.

## Overview

A brain is the runtime that calls the model and keeps the session. A hand is a process on another machine that runs Code Mode cells with Claude's tools and the workspace tools. Either side can dial, and the protocol is the same in both directions.

The model picks a hand per call. A nested call whose arguments include `hand: "build-box"` runs on that hand, and the `hands` tool lists the attached hands. A Rust runtime or a nanocode-wasm session can be the brain. Hands run only from Rust or `nc hand`.

```mermaid
flowchart LR
  brain[Brain: inference, subagents, durability] <-->|WebSocket| laptop[Hand: laptop]
  brain <-->|WebSocket| build[Hand: build-box]
  brain --> store[(Session store)]
```

## Walkthrough

The steps below build a brain that listens on a loopback port and a hand that dials it. Both read a shared token from `NANOCODE_RUNNER_TOKEN`.

### Share a Token

The brain and every hand authenticate with the same token. Share it out of band.

```bash
# Generate a token once and export it on both machines
export NANOCODE_RUNNER_TOKEN=$(openssl rand -hex 32)
```

### Start the Brain

In Rust, `runner_server(...)` makes the runtime listen for hands. By default, hands run alongside the brain's local tools, and `.replace()` runs every tool on the hand instead.

In TypeScript, `hands: true` lets the session accept hands, and the host owns the sockets. Each hand gets a `RemoteExecutor` and a connection ID. `attach(hand, connection)` registers it under its name, and `remote.execute` routes each request by `request.connection`.

#### Rust

```rust title="brain/src/main.rs"
use nanocode::{
    ClaudeClient, Nanocode, OpenAi, RunnerServerConfig, Tools,
};

type Result<T> =
    std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;

#[tokio::main]
async fn main() -> Result<()> {
    // 1. Listen for hands on a loopback port with the shared token
    let token = std::env::var("NANOCODE_RUNNER_TOKEN")?;
    let runtime = Nanocode::builder()
        .openai(OpenAi::default())
        .claude(ClaudeClient::default())
        .tools(Tools::builder().without_defaults().build()?)
        .without_cua()
        .runner_server(
            RunnerServerConfig::listen("127.0.0.1:9000").token(token),
        )
        .build_async()
        .await?;

    // 2. Ask for work on a named hand
    let (agent, _events) = runtime.spawn().build().await?;
    let turn = agent
.prompt("List the attached hands, run cargo test on build-box and summarize any failures.")
.await?;
    println!("{}", turn.result().await?.final_message());

    agent.flush().await?;
    runtime.shutdown().await?;
    Ok(())
}
```

#### TypeScript

```ts title="brain.ts"
import {
  RemoteExecutor,
  RemoteRefusal,
  sessionUuid,
} from 'nanocode-wasm'

const policy = { max_frame_bytes: 1 << 20, timeout_ms: 10_000 }
const hands = new Map<string, RemoteExecutor>()

// 1. Open the session for hands, routing each request to its connection
await runtime.open(
  { ...config, hands: true },
  {
    dispatch,
    remote: {
      cancel: (id) => hands.forEach((hand) => hand.cancel(id)),
      dispose: async () => {
        await Promise.all(
          [...hands.values()].map((hand) => hand.dispose()),
        )
      },
      execute: (request, id) => {
        const hand = hands.get(request.connection ?? '')
        if (!hand) throw new RemoteRefusal('hand is not attached')
        return hand.execute(request, id)
      },
    },
  },
)

// 2. Accept an authenticated hand socket and attach it under its
// catalog name
export async function accept(socket: WebSocket) {
  const executor = RemoteExecutor.accept(socket, policy)
  socket.addEventListener('message', (event) =>
    executor.receive(event.data),
  )
  socket.addEventListener('close', () => executor.disconnected())
  await executor.ready()

  const connection = sessionUuid()
  hands.set(connection, executor)
  const { name } = await runtime.identity(
    executor.catalog!,
    policy.max_frame_bytes,
  )
  await runtime.attach(name ?? connection, connection)

  // 3. Detach the hand when its socket closes
  executor.closed.then(() => {
    hands.delete(connection)
    return runtime.detach(connection)
  })
}
```

A hand that dials sends the token as `Authorization: Bearer` in the WebSocket handshake. A TypeScript brain checks that header before it accepts the socket. `submit` takes an optional `hand` name that selects the hand for that turn.

### Start a Hand

A hand is a Rust program or `nc hand`. `nanocode::hand::connect` builds a hand from a URL, a token, and a starting directory. `run` dials the brain, reconnects after a dropped connection, and reports each `HandEvent` to the callback.

```rust title="hand/src/main.rs"
type Result<T> =
    std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;

#[tokio::main]
async fn main() -> Result<()> {
    // Dial the brain and run cells in the current directory as build-
    // box
    let url = std::env::args()
        .nth(1)
        .ok_or("usage: hand ws://BRAIN:PORT")?;
    let token = std::env::var("NANOCODE_RUNNER_TOKEN")?;

    nanocode::hand::connect(url, token, std::env::current_dir()?)?
        .name("build-box")
        .run(|event| eprintln!("{event:?}"))
        .await?;
    Ok(())
}
```

A hand's name defaults to the machine's host name. The hand runs until the brain revokes it, or replaces it with a newer hand of the same name.

## Recipes

### Use the CLI

`nc hand` runs a hand without a Rust program, and `--runner-host` makes `nc` listen as the brain. Both read the token from `NANOCODE_RUNNER_TOKEN`.

```bash
# 1. On the brain
nc --runner-host 127.0.0.1:9000

# 2. On each hand, starting in a checkout
nc hand ws://127.0.0.1:9000 --name build-box -C ~/src/app
```

| Flag                          | Behavior                                                     |
| ----------------------------- | ------------------------------------------------------------ |
| `--runner-host ADDR`          | The brain listens for a hand                                 |
| `--runner-connect WS_URL`     | The brain dials a hand started with `nc hand --listen`       |
| `--runner-mode replace`       | Runs every tool on the hand instead of alongside local tools |
| `--runner-call-timeout-ms MS` | Sets a deadline for each remote tool call                    |
| `nc hand --listen ADDR`       | The hand waits on a loopback address for a brain to dial     |
| `nc hand --cua`               | The hand also serves computer use                            |

### Listen for Brains

`nanocode::hand::listen` builds a hand that accepts brains on a loopback address instead of dialing.

```rust
// Wait for a brain to dial ws://127.0.0.1:9100
let address = "127.0.0.1:9100".parse()?;
nanocode::hand::listen(address, token, std::env::current_dir()?)?
    .run(|event| eprintln!("{event:?}"))
    .await?;
```

### Pick Hand Tools

`nanocode::hand::tools(directory)` returns the default hand tools, Claude's tools plus the workspace tools. `Hand::connect` takes any `Tools` value, for example these tools with Rust tools added.

```rust
use nanocode::hand::Hand;

// The default hand tools plus one registered Rust tool
let tools = nanocode::hand::tools("/srv/app")?
    .into_builder()
    .tool(checked_sum)
    .build()?;
Hand::connect("ws://127.0.0.1:9000", token, tools)
    .directory("/srv/app")
    .run(|event| eprintln!("{event:?}"))
    .await?;
```

### Failure Rules

A remote call never falls back silently:

* **Disconnected hand:** A cell that selects a disconnected hand fails without running anything. nanocode does not fall back to another machine.
* **Dropped connection:** A call in flight reports an unknown outcome instead of replaying work that the hand may have done.
* **Reconnects:** A hand redials with a backoff until the brain revokes it. Both sides ping every 20 seconds.
* **Parallel hands:** `Promise.all` across hands settles each call separately.

Between machines, put a TLS proxy in front of the listener and use `wss://`.

## More

- [Code Mode](/docs/guides/code-mode): Write the cells that hands run.

- [Durability](/docs/guides/durability): Keep the brain's sessions across restarts.

- [RemoteExecutor](/docs/api/wasm/RemoteExecutor): Reference for the TypeScript hand transport.

- [Cloudflare Workers](/docs/guides/workers): Deploy nanocode on Cloudflare Workers.

- [hand](/docs/api/rust/hand): Reference for connect, listen, and tools.

- [CLI](/docs/api/cli): Reference for nc hand and the runner flags.
