---
slug: "rhinoclaw-x-4"
source_type: "clawhub"
source_url: "https://clawhub.ai/skills/rhinoclaw"
repo: ""
source_file: "description"
---
---
name: rhinoclaw
version: 0.7.2
description: Control Rhino 3D via the RhinoClaw plugin over TCP. Use whenever you need to create, modify, query, measure, or render 3D geometry, manage layers/materials, run Boolean and transform ops, drive Grasshopper definitions, work with VisualARQ BIM objects, or batch multiple steps as one atomic operation. Requires Rhino 7/8 running with `tcpstart` (the plugin's command line server) on the Rhino host.
---

# RhinoClaw Skill

Direct TCP control of Rhino 3D from an OpenClaw agent. Plugin is
running on a Windows or macOS Rhino instance; this skill talks to it from
WSL / Linux / wherever you live.

> **Step 0 — ALWAYS run `preflight` first.** One call returns the complete
> connection/auth state and an exact `next_action`. Only call other tools when
> `data.auth == "ready"`. Then read § "Decision Tree" to pick the cheapest tool.

---

## 0. Preflight & error-code playbook (read before touching auth)

**Never trial-and-error the connection.** Run `preflight` (MCP) — or, on the raw
TCP path, `hello` then a single `ping` — and act on the result. `hello` is
**auth-free**: it reports the server's state even with no/wrong token and even
while blocked, without tripping the brute-force counter.

| `preflight.data.auth` / `error_code` | Meaning | Do exactly this |
|---|---|---|
| `ready` | Connected, auth ok | Proceed. |
| `missing_client_token` / `AUTH_REQUIRED` (no token) | Plugin wants a token, client sends none | Set `RHINOCLAW_AUTH_TOKEN` on the client (same value as the plugin). **Do NOT retry blindly.** Using the deployed skill? Re-sync it: `scripts/sync-skill.sh`. |
| `token_mismatch` / `AUTH_REQUIRED` (wrong token) | Both sides have a token, they differ | Make `RHINOCLAW_AUTH_TOKEN` identical on both sides, then **restart Rhino** (the plugin reads the token only at start). |
| `blocked` / `AUTH_BLOCKED` | Too many failed auth attempts | Wait until `blocked_until`. **Do NOT retry** — retries extend the cooldown. |
| not connected / `CONNECTION_REFUSED` | Server not running / wrong host:port | Open Rhino, run `tcpstart` (remote) or `mcpstart` (local). On WSL the Windows host IP changes per session — check `RHINOCLAW_HOST`. |
| `TIMEOUT` | Command reached Rhino but blocked the UI thread / too slow | Command-specific — pass a larger `timeout`. **Not** an auth problem; do not re-auth. |

Rules of thumb:
- **Auth errors are configuration errors, not transient** — never loop-retry them.
- The plugin reads `RHINOCLAW_AUTH_TOKEN` **only at start** — changed the env after
  Rhino was running? Restart Rhino (or the RhinoClaw server).
- One source of truth: `~/projects/RhinoClaw`. The deployed skill at
  `~/clawd/skills/rhinoclaw` is generated by `scripts/sync-skill.sh` — if `preflight`
  or `hello` look stale, re-sync.

---

## Decision Tree — pick the cheapest tool that works

```
        ┌──────────────────────────────────┐
        │  What do you need to do?         │
        └──────────────────────────────────┘
                       │
   ┌───────────────────┼───────────────────┐
   ▼                   ▼                   ▼
 single op          many ops at once    inspect / measure
   │                   │                   │
   ▼                   ▼                   ▼
 typed module      batch_operations    scene_summary
 (geometry,        with rollback       find_nearby
  transforms,                          get_relationships
  booleans,                            get_object_info
  layers,
  materials,
  visualarq…)
```

**Preference order — always try in this order, drop one level only when the
previous can't express what you need:**

1. **Typed Python helper** in `scripts/rhinoclaw_client/<module>.py`
   (`geometry.create_box`, `booleans.union`, `transforms.copy`, …).
   Validated, undo-aware, never executes arbitrary code.
2. **`batch_operations`** when you need ≥2 steps in one atomic shot
   (rollback / abort / continue / best-effort policies).
3. **`script_exec.run_python`** with `import rhinoscriptsyntax as rs`
   (`rs.AddSphere`, `rs.LayerNames`, …). Use when no typed helper
   exists. RhinoScript is well documented and agent-friendly.
4. **RhinoCommon directly** via `script_exec.run_python`. Use when
   `rhinoscriptsyntax` doesn't expose what you need. Cheatsheet at
   `docs/rhinocommon-cookbook.md` in the dev repo.
5. **`run_native_command`** — fires `RhinoApp.RunScript("_Loft …")`.
   Last-resort hatch for commands that have no RhinoCommon equivalent
   (or where the native one is much simpler). Allowlisted by the
   plugin.

If you don't know what tools exist, call `list_capabilities` once at
session start. It returns a categorised inventory of typed tools,
helpers, native-command allowlist, and RhinoCommon cookbook topics.

---

## Prerequisites

1. **Rhino 7/8** running on Windows (or macOS).
2. **RhinoClaw plugin** ≥ 0.2.8 installed.
3. **Plugin server started**:
   - `mcpstart` for local-only AI clients (Cursor / Claude Desktop).
   - `tcpstart` for remote / WSL / raw-TCP clients.
4. **Auth token** (recommended for tcpstart): set `RHINOCLAW_AUTH_TOKEN`
   on **both** the Rhino host and the client side. See SECURITY.md and
   `scripts/setup-auth-token.ps1` in the repo.

---

## Configuration

`config.json` next to the scripts:

```json
{
  "connection": {
    "host": "172.31.96.1",
    "port": 1999,
    "timeout": 15.0
  }
}
```

All four fields can be overridden by env vars: `RHINOCLAW_HOST`,
`RHINOCLAW_PORT`, `RHINOCLAW_TIMEOUT`, `RHINOCLAW_AUTH_TOKEN`. The
`rhino_client.py` module attaches the auth token to every TCP command
automatically — agent never has to manage it manually.

---

## Quick Test — run `preflight` first

```bash
cd ~/clawd/skills/rhinoclaw/scripts
python3 rhino_client.py preflight   # one call → connection/auth state + next_action
# → {"connected": true, "auth": "ready", "plugin_version": "0.5.0.0", ...}
```

Act **only** when `"auth": "ready"`. Otherwise follow `next_action` exactly
(see §0). `python3 rhino_client.py hello` shows the raw auth-free handshake;
`python3 rhino_client.py ping` is the authenticated liveness check.

### TCP = MCP parity — what raw TCP can do

The raw client reaches **every plugin command** via the generic
`send_command(cmd_type, params)` — the same capabilities as the MCP server:

```python
from rhino_client import RhinoClient
with RhinoClient() as c:
    c.preflight()                                   # connection/auth verdict
    c.send_command("create_object", {"type": "BOX", "params": {...}})
    c.send_command("build_and_bake_gh", {...})      # author + solve + bake a .gh
    c.build_and_bake_recipe("box", "C:/tmp/b.gh",   # native parametric recipe
                            params={"x": 40, "y": 20, "z": 10}, layer="Boxes")
    c.build_and_bake_recipe("list")                 # list recipes
```

Client-side high-level helpers mirror the MCP tools: `preflight()`, `hello()`,
`build_and_bake_recipe()`. Everything else is a direct `send_command` (the
plugin dispatches it). **Recipes live in the plugin**, so MCP and TCP behave
identically. Requires a plugin build with these commands (0.5.0+ after rebuild).

---

## Cookbook — common workflows

Five concrete recipes. Adapt freely; always prefer typed helpers over
inline Python where possible.

### Recipe 1 — "Create a layered box with a material" (atomic)

```python
from rhino_client import RhinoClient
client = RhinoClient(); client.connect()

result = client.send_command("batch_operations", {
    "name": "Layered box",
    "on_error": "rollback",
    "steps": [
        {"tool": "create_layer",
         "args": {"name": "Walls", "color": [180, 180, 180]}},
        {"tool": "create_material",
         "args": {"name": "Concrete", "color": [200, 200, 200]}},
        {"tool": "assign_material_to_layer",
         "args": {"layer_name": "Walls", "material_id": 0}},
        {"tool": "create_object",
         "args": {"type": "BOX", "layer": "Walls",
                  "params": {"width": 5, "length": 3, "height": 2.5}}},
    ],
})
```

### Recipe 2 — "Find every object near origin and inspect relationships"

```python
nearby = client.send_command("find_nearby", {
    "point": [0, 0, 0], "radius": 100, "layer": "Walls",
})
for wall in nearby["results"]:
    relations = client.send_command("get_relationships", {
        "object_id": wall["id"],
        "touch_tolerance": 0.05,
    })
    print(wall["name"], "touches", len(relations["touching"]), "objects")
```

### Recipe 3 — "Boolean a hole through a wall, leave geometry on failure"

```python
# `abort` policy: stop on first failure but keep completed steps. Lets
# the user inspect/fix manually instead of an automatic rollback.
client.send_command("batch_operations", {
    "on_error": "abort",
    "steps": [
        {"tool": "create_object", "args": {"type": "CYLINDER", "params": {...}}},
        {"tool": "boolean_operation",
         "args": {"operation": "difference",
                  "primary_id": "<wall>", "secondary_ids": ["<cylinder>"]}},
    ],
})
```

### Recipe 4 — "Run a Grasshopper definition with the file's own defaults"

```python
# Inspect first to discover what the GH file actually wants — never guess.
info = client.send_command("inspect_grasshopper_definition", {
    "file_path": r"C:\path\to\Door.gh",
    "only_player_inputs": True,
})
print("Inputs:", [i["name"] for i in info["inputs"]])

# Run with the defaults the GH file already exposed:
client.send_command("run_grasshopper_interactive", {
    "file_path": r"C:\path\to\Door.gh",
    "inputs": {i["name"]: i.get("default")
               for i in info["inputs"] if i.get("default") is not None},
})
```

### Recipe 5 — "Use a native Rhino command when nothing else fits"

```python
# rhinoscriptsyntax has no clean Sweep1 wrapper — use the underlying
# native Rhino command. Plugin allowlists known-safe commands.
client.send_command("run_native_command", {
    "command": "_Sweep1",
    "args": ["_Pause", "_Pause", "_Enter"],
})
```

---

## RhinoCommon and rhinoscriptsyntax — when typed tools don't reach

When you need something no typed RhinoClaw tool covers, you have two
scripting paths via `script_exec.run_python`:

### Option A — `rhinoscriptsyntax` (start here)

Friendlier API, well documented, almost-Rhino-Python-Console-equivalent.
Find functions:

- Online: https://developer.rhino3d.com/guides/rhinopython/
- Indexed locally: `get_rhinoscript_python_function_names(["surface", "curve"])`
  returns matching function names.
- `get_rhinoscript_python_code_guide("AddLoftSrf")` returns the parameter
  signature + example.

Example: get all object IDs on a layer

```python
client.send_command("execute_rhinoscript_python_code", {
    "code": "import rhinoscriptsyntax as rs; print(rs.ObjectsByLayer('Walls'))",
})
```

### Option B — RhinoCommon directly

Mächtiger, stricter API. Use the cheatsheet at
**`docs/rhinocommon-cookbook.md`** in the dev repo for hot-path patterns
(Brep ops, Curve math, Mesh conversion, Transform composition,
Intersection events, …).

When you need a function not in the cookbook:

1. Search https://developer.rhino3d.com/api/rhinocommon/?searchtext=<keyword>
2. Note the namespace — most common: `Rhino.Geometry`, `Rhino.DocObjects`,
   `Rhino.Display`, `Rhino.Render`.
3. Write a tiny test via `execute_python3_code` BEFORE wiring into a long
   pipeline.

Example (mesh from Brep):

```python
client.send_command("execute_python3_code", {
    "code": """
import scriptcontext as sc
import System
from Rhino.Geometry import Mesh, MeshingParameters
brep = sc.doc.Objects.Find(System.Guid.Parse('<id>')).Geometry
meshes = Mesh.CreateFromBrep(brep, MeshingParameters.Default)
print('mesh count:', len(meshes))
""",
})
```

---

## Negative examples — what NOT to do

- ❌ `rs.command("_Move ...")` — interactive command, blocks the server.
  Use the typed `move_object` / `transforms.move` instead, or
  `run_native_command` for non-interactive scripts.
- ❌ Many separate `create_object` calls when `batch_operations` works.
  TCP latency dominates; batching is 10× faster on Tailscale.
- ❌ `execute_rhinoscript_python_code` for a single primitive — that's
  what the typed tools are for. Save scripting for when typed tools
  can't express the operation.
- ❌ Guessing at GH parameter names. Always
  `inspect_grasshopper_definition(only_player_inputs=True)` first.
- ❌ Acting on object IDs you didn't just receive. If unsure, call
  `get_object_info` or `find_nearby` first — the user might have
  deleted, locked, or hidden things since your last call.

---

## Scripts Reference

| Script | Purpose |
|--------|---------|
| `rhino_client.py` | Base TCP client; auto-attached auth token + retry |
| `geometry.py` | Primitives (box, sphere, cylinder, point, line, curve, …) |
| `transforms.py` | Move, rotate, scale, copy, mirror, arrays |
| `booleans.py` | Union, difference, intersection |
| `solids.py` | Solid-specific ops (extrude, fillet edges, shell) |
| `surfaces.py` | Loft, sweep, revolve, patches |
| `curves.py` | Offset, fillet, chamfer, join, explode |
| `selection.py` | Select / list by layer, type, name, IDs |
| `analysis.py` | Object info, properties, bounding box, volume, distance |
| `scene.py` | Document-level info, scene summary, find_nearby |
| `layers.py` | Create / set / list / delete / lock / hide layers |
| `materials.py` | PBR materials, assignments |
| `viewport.py` | Views, camera, screenshots |
| `render.py` | Lights, render settings, render-to-file |
| `groups.py` | Create / ungroup / list groups |
| `objects.py` | Generic object ops (delete, rename, recolor, layer change) |
| `text.py` | Text, text dots, leaders, annotations |
| `files.py` | Open / save / export / import |
| `grasshopper.py` | Run / inspect / build / bake GH definitions |
| `script_exec.py` | Inline Python (rhinoscriptsyntax + RhinoCommon) |
| `visualarq.py` | VisualARQ BIM objects (walls / doors / windows / IFC) |
| `presets.py` | Reusable parameter sets |
| `utils.py` | Common helpers (logging, validation) |

---

## Configuration files

- `config.json` — runtime defaults (host, port, timeout, …).
- `config/presets.yaml` — domain presets (door types, wall thicknesses).
- `config/templates.yaml` — full pipeline templates.
- `references/commands.md` — quick command lookup.

---

## Troubleshooting

| Symptom | First check |
|---------|-------------|
| `AUTH_REQUIRED` | `echo $RHINOCLAW_AUTH_TOKEN` matches the value on the Windows side? Both sides restarted after setting the env var? |
| `Connection refused` | `tcpstart` already typed in Rhino's command line? Plugin loaded? Try `mcpstatus`. |
| `Connection timed out` | `RHINOCLAW_HOST` correct? On WSL the Windows host IP changes per session. `ip route show \| awk '/default/ {print $3}'` from WSL gives current value. |
| `AddressAlreadyInUse` (port 1999) | Old Rhino zombie. `Get-NetTCPConnection -LocalPort 1999` in PowerShell on the host. |
| Tool returns success but nothing visible | Likely on a hidden layer or off-screen. Run `scene_summary` + `zoom_extents`. |
| Long script "hangs" | `cancel_rhino_command()` is the last resort. Default timeout is 15s; pass `timeout=60` for long ones. Server-side max is `RHINOCLAW_MAX_TIMEOUT=120`. |

---

## Reference

- **Plugin source / dev repo:** https://github.com/McMuff86/rhinoclaw
- **End-user release repo:** https://github.com/McMuff86/rhinoclaw-release
- **Issues:** https://github.com/McMuff86/rhinoclaw/issues
- **RhinoCommon API:** https://developer.rhino3d.com/api/rhinocommon/
- **Rhino.Python guide:** https://developer.rhino3d.com/guides/rhinopython/
- **rhinoscriptsyntax index:** https://developer.rhino3d.com/api/RhinoScriptSyntax/
