---
title: Simulator networking and management
description: Reach apps on a VM, update its OS, manage VMs, and pass a serial device through.
---

This page continues from [Simulator](../../installation/wendyos-virtual-machine/), which covers what the simulator is and how to create and start a VM.

## Trying a change before it merges

Every pull request to WendyOS-Builder builds a VM image, so a change can be run
without hardware and without waiting for a release:

```sh
wendy vm create review --pr 1234
wendy vm start review
```

`--pr` takes the pull request number and stands in for the version: a PR
publishes exactly one build, so it cannot be combined with `--version`,
`--nightly` or `--image`.

<Callout type="warn">
A pull request's image is built from an unmerged branch, so booting one runs
code that has not been reviewed — on your machine, with a real filesystem and a
real network stack. Treat `--pr` the way you would treat running that branch's
build script: only for branches you trust. `wendy vm create --pr` says the same
thing when it starts.
</Callout>

## Networking

`wendy vm start` offers two networks. Both are discoverable from your own
machine; the difference is whether anything *else* can reach the VM.

| `--net` | Setup needed | Listed by `wendy discover` | On your network |
| --- | --- | --- | --- |
| `user` (default) | none | Yes — Simulator tab | No |
| `shared` | one-time `socket_vmnet` install, **macOS only** | Yes — Local tab via mDNS | Host-shared segment |

The default forwards the agent's port to your machine and needs no privileges.
Nothing on your network can reach the guest. Its mDNS announcement does leak
out (QEMU passes the guest's multicast through one way), advertising an address
nobody can dial — `wendy discover` recognises that sighting as your VM and
keeps it off the **Local** tab. A user-mode VM is listed only on the
**Simulator** tab, which shows stopped ones too; in JSON output it is under
`simulators` as `vm:<name>`.

Choose `shared` to test mDNS or reach the guest directly by IP on the host's
vmnet segment. Its reachable entry stays on **Local** (and in JSON's
`lanDevices`); the **Simulator** tab still lists its local lifecycle state.
This is not a bridge onto the physical LAN; reachability from another machine
depends on routing and the helper's configuration:

```sh
wendy vm start dev --net shared
```

The helper it needs is `socket_vmnet`; if it is missing, `wendy` offers to run
`brew install socket_vmnet` and `sudo brew services start socket_vmnet` for you.
Both are also fine to run by hand.

<Callout type="warn">
`--net shared` is macOS only — it builds on Apple's vmnet framework. On Linux
use the default `--net user` and reach the VM by address.
</Callout>

<Callout type="info">
On macOS 15 and later the first `wendy discover` may ask for **Local Network**
permission. Discovery cannot see anything until you allow it.
</Callout>

### Reaching an app you deployed

`--net user` puts the guest on a private NAT. The address the guest sees itself
at — something like `10.0.2.15` — does not exist on your machine, and nothing
routes into that network. Only forwarded ports are reachable, and the launcher
forwards three:

| Port | Carries |
| --- | --- |
| `50051` | the agent |
| `50052` | the agent again, once the device is provisioned and on mTLS |
| `5000` | the device's container registry, which `wendy run` pushes to |

On macOS and Linux, `wendy run` also forwards each declared `http` entitlement
and TCP readiness port to the same port on `127.0.0.1`. The app URL uses that
loopback address, and readiness checks use it too. This works with background
VMs and detached applications; forwards last until the VM stops and are
recreated on the next run.

For an app with a declared [`http` endpoint](../../reference/app-configuration/#http), capture
the detached result and fetch the reported host URL:

```sh
wendy --json --device vm:dev run --yes --detach > deploy.json
url=$(jq -er '.url' deploy.json)
curl --fail --retry 10 --retry-connrefused --retry-delay 1 --max-time 5 "$url"
```

`vm:dev` selects the device; it is not a DNS hostname. The result's
`readiness: "not_checked"` means start was acknowledged, so check the response
against your app's expected behavior. If `url` is absent, inspect the declared
HTTP endpoints, forwarding and logs. See the [detached result reference](../../advanced/clients/wendy-cli/commands/run/#detached-output)
for fields and multi-service output.

If another application or VM owns the host port, deployment reports a forwarding
error. Free that port or choose a different application port. Other ports are
not automatically exposed. VMs started with an older launcher must be restarted
once to enable this feature. On `--net shared` the guest is on your network
directly and none of this applies.

The launcher manages these forwards through a Unix control socket in the VM's
directory, whose permissions are enforced as `0700` on each launch. There is no
TCP control listener. Other user accounts cannot traverse the directory, but
processes running as your user (and root) can connect and have full QEMU control.

### If port 50051 is taken

Prefer `--device vm:<name>` (or save it with `wendy device set-default
vm:<name>`) over a literal loopback address. The alias follows the VM's current
port and keeps enrolled-device trust pins separate for each VM, so two VMs on
different ports never share one. While a VM is running, a literal
`127.0.0.1:<port>` on its forwarded agent port, or on the mTLS port after it,
is that VM too: it is pinned as `vm:<name>`, and reconnects to the VM by name
after an agent update.

Every other loopback address (another port, `localhost`, `::1`) shares the pin
of its bare host (`127.0.0.1`, `localhost` or `::1`), as it always has, whatever
the port. That includes the VM's own addresses once the VM is stopped. A
connection to a running VM through its typed `127.0.0.1` address also records
the VM's identity in the shared `127.0.0.1` pin when that pin is not set yet,
or names only the VM's organization, so the address stays protected while the
VM is stopped; it never replaces a `127.0.0.1` pin that names another device.

To replace an enrolled VM intentionally, run `wendy device unpin vm:<name>`
before connecting to the replacement. It clears only the VM's own pin, never
the shared `127.0.0.1` pin that other loopback addresses use. That pin is
cleared by `wendy device unpin 127.0.0.1`.

The agent's port is often already in use on a development machine. `wendy vm
start` and `--device vm:<name>` then move the host side of the forward to the
next free port on their own, and the VM keeps that port for later starts. To
choose it yourself, pass `--port`; a port given that way that is in use is an
error rather than a hint to move:

```sh
wendy vm start dev --port 50151
wendy --device vm:dev device info
```

The alias follows the VM to its new port.

## Updating the OS

The VM is partitioned exactly like a device — two rootfs slots plus a growable
`/data`, with GRUB choosing between them through `grubenv` — so A/B OTA works
here the same way it works on hardware:

```sh
wendy --device vm:dev os update
```

The update is written to the slot that is not running, the VM reboots into it,
and the new slot is committed once the agent comes back. `/data` is untouched,
so containers and app data survive.

`device info` reports both slots under `osUpdate.engine` — `currentSlot` is the
one running, and each entry names its partition, so you can watch A on
`/dev/vda3` hand over to B on `/dev/vda4`:

```sh
wendy --device vm:dev device info
wendy --device vm:dev os update-status   # how the last update ended
```

The agent returns on the same forwarded port after the reboot, so nothing needs
restarting on your side. `wendy vm logs <name>` shows the reboot and the slot
GRUB picked, which is the quickest way to watch an update land.

<Callout type="info">
This makes the simulator the cheapest way to rehearse an OS update — including a
rollback — without tying up a Jetson or a Raspberry Pi.
</Callout>

## Managing VMs

`wendy vm` is hidden from `wendy --help` on purpose — the simulator is meant to
be reached through `wendy run` or `--device sim`. The subcommands below are all
still available for when you need to look under the hood.

```sh
wendy vm create        # prompt for a VM name and simulator profile
wendy vm list          # every VM, with its state, address and version
wendy vm start dev -d  # run it in the background instead of taking the terminal
wendy vm logs dev -f   # the console of a backgrounded VM
wendy vm stop dev      # request graceful guest shutdown
wendy vm rm dev        # delete a VM and its disk
```

`wendy vm create` prompts for a name and a profile: Generic WendyOS, Unitree Go2,
Unitree G1, LeRobot SO-101, Yahboom ROSMASTER R2, or Wendy Lite (WASM or native).
Supply the name or `--profile generic|go2|g1|so101|rosmaster-r2|wendy-lite|wendy-lite-native`
to skip that prompt.
For scripts, pass the name explicitly; the profile defaults to `generic` without
an interactive terminal, with `--json`, or with `--yes`.

The VM name is optional for `start`, `stop`, `logs`, and `rm` in an interactive
terminal. Leave it out to choose a VM from a list; scripts should continue to
pass the name explicitly.

`vm stop` waits up to five minutes for the guest to stop its apps, shut down and
flush its filesystems. If QMP is unavailable
or the guest is still shutting down after that, it reports an error without
killing the VM. Shut down inside an older guest without QMP, or
use `wendy vm stop dev --force` only when an immediate power cut is intended.

A backgrounded VM outlives the command that started it, so closing the terminal
or pressing Ctrl-C does not power it off. `wendy vm rm` refuses to delete a
running VM unless you pass `--force`.

VMs live in `~/.wendy/vms/<name>/`. Removing one deletes its disk, which cannot
be undone — but the image you created it from is untouched, so you can always
make a fresh one.

Published VM images are cached by their manifest SHA-256, not by the version
label. Cached and newly downloaded bytes are verified before use, so rebuilding
a PR under the same label cannot silently reuse the old image. If no checksum
is published, each create downloads afresh and removes that temporary download
after provisioning. Existing VM disks are never refreshed implicitly.

New VMs receive a random locally-administered MAC stored in `meta.json`, stable
across restarts and distinct even when another host creates a VM with the same
name. Older VMs without a stored MAC retain their legacy name-derived address
to avoid changing their network identity on upgrade. If two such legacy VMs
share a name, do not place both on the same shared network; recreate one to
allocate a fresh address. Copying an entire VM directory also copies its MAC.

<Callout type="info">
A freshly created VM's agent is unauthenticated until the device is provisioned,
and it is reachable on `127.0.0.1`. On a machine you share with other people,
that means any local account can talk to it — including enrolling it — in the
window before you do. Provision it promptly, or use a machine you do not share.
</Callout>

## Options

| Flag | Default | Purpose |
| --- | --- | --- |
| `--image` | — | Create from a local `.wic` instead of downloading |
| `--version` | latest | Version to download |
| `--nightly` | off | Download a nightly rather than a release |
| `--disk` | `16` | Disk size in GiB |
| `--net` | `user` | `user` or `shared` — see above |
| `--port` | `50051` | Host port forwarded to the agent |
| `--memory` | `4096` | Guest memory in MiB |
| `--cpus` | `4` | Guest CPU count |
| `--detach` / `-d` | off | Run in the background rather than on this terminal |
| `--yes` / `-y` | off | Accept prompts, including installing QEMU on macOS and Windows |

## Serial passthrough

A Windows, macOS or Linux host can forward one serial port to a WendyOS ARM64 VM
as a QEMU USB serial adapter. Install the adapter's driver if your host needs one,
find its host name and the exact baud required by your device, then save the
mapping. For example (replace the name, port and baud):

| Host | Port looks like | Find it with |
|---|---|---|
| Windows | `COM12` | Device Manager → Ports (COM & LPT) |
| macOS | `/dev/cu.usbmodem1101` or `/dev/cu.usbserial-…` | `ls /dev/cu.*` before and after plugging in |
| Linux | `/dev/serial/by-id/usb-…` (stable) or `/dev/ttyACM0` | `ls -l /dev/serial/by-id/` |

```sh
wendy vm serial my-vm --port /dev/cu.usbmodem1101 --baud 1000000
wendy vm serial my-vm
wendy vm start my-vm
```

The first command saves `serial.json` inside this VM's directory under the Wendy
configuration directory. The second prints the mapping as JSON (`null` if none).
Each foreground start loads the same mapping. Stop the VM before changing it.
To disable passthrough, run `wendy vm serial my-vm --clear` while it is stopped.
Configuration does not open the port; availability and driver support are checked
at the next start. A mapping saved on one host type is rejected on another.

On macOS use the `cu.` node rather than `tty.`. On Linux, prefer the
`/dev/serial/by-id/` name: `ttyACM0` can change when devices are replugged. If
the start reports permission denied, add your user to the device's group
(usually `dialout`) and log in again.

The host line is fixed at the requested baud, **8 data bits, no parity, one stop
bit, no handshake, DTR and RTS disabled**. Wendy submits the exact baud to the
host driver and never selects a nearby baud: a rate the driver refuses fails the
start. Windows also checks the driver's reported settings. A driver can still
round the rate internally, so no host check proves the adapter's actual
electrical baud. Guest termios baud changes and modem-control commands do not
change the host line. The bridge forwards binary bytes in both directions without
line processing.

The guest's existing `ftdi_sio` driver normally exposes `/dev/ttyUSB0`. Check the
actual guest path, then grant your app its serial entitlement, for example:

```json
{"type":"serial","device":"ttyUSB0"}
```

No guest image modification is required. Keep `wendy vm start` open in its
terminal and use a second terminal for ordinary app commands targeting
`--device vm:my-vm`. The bridge runs inside that launcher, so background starts
(`--detach`) and automatic starts through workflows that require detaching are
refused for a VM with a mapping; start it explicitly first. On macOS and Linux,
`wendy vm stop my-vm` shuts it down as usual. Windows does not yet have the
managed QMP monitor: shut down inside the guest, use Ctrl-A then X, or
explicitly cut power with `wendy vm stop my-vm --force`.

Each VM gets a dynamically allocated listener bound only to `127.0.0.1`; the
listener closes after QEMU connects. There is no LAN listener or fixed bridge
port. Local processes are trusted: this TCP connection has no authentication.
The port is held exclusively while the VM runs, so two VMs cannot share it. On
macOS and Linux Wendy also takes the advisory lock that pyserial-based tools
(`esptool`, `idf.py monitor`) use, so they see the port as busy. Different VMs
may use different ports (and must also have distinct forwarded agent ports,
using `vm start --port` where necessary).

Normal stop, QEMU exit and startup failures release the port and listener. A
serial I/O error or cable disconnect that the host driver reports stops QEMU
and prints an actionable error; reconnect the device and restart the VM. There
is no silent reconnect. If the launcher is forcibly terminated, check for a
remaining QEMU process before starting again. On macOS and Linux, use
`wendy vm stop my-vm`; on Windows, check Task Manager.

### Check your serial connection

After starting the VM, confirm that your app can open the guest serial device
and exchange the data it expects. Saving a mapping does not verify the adapter,
its baud rate, or the attached hardware. Start with your device's documented
read-only checks before enabling any physical controls.

If the cable disconnects or a serial I/O error stops the VM, reconnect the device
and start the VM again. If another program reports the port busy, stop the VM
before using the port elsewhere. One serial device can be mapped per VM;
this does not provide general USB, camera, or GPU passthrough.
