Where VMs Run
A sand VM doesn't have to run on the machine in front of you. It can run:
- On this machine — the default.
sandruns Lima locally, and nothing needs configuring. - On another machine, over SSH —
sandruns the same Lima commands on a host you can SSH into, so VMs live on a bigger box (a workstation, a home server) while you work from a laptop. - On a Proxmox VE host —
sanddrives VMs through the Proxmox REST API with a pool-scoped token. No Lima involved. See Proxmox VE Setup for the one-time host setup.
Whichever you pick, the commands, the board, and the keybindings are the same. Only the machine underneath changes.
Every location sand knows about is a Connection Profile. Profiles are
the only configuration surface for this: there is no environment variable to
set and no per-invocation flag needed to pick a backend (older sand builds
used SAND_PROVIDER / SAND_REMOTE_* environment variables for this; those
are removed — see Environment variables
removed below).
What a profile is
A profile has:
- A name — a short, renameable label you pick (
work,home-server,local). Names don't need to be unique, though the TUI won't stop you from picking a name that collides with another profile's — it's a display label, not an identifier. - A type —
local,remote-ssh, orproxmox. - Connection details — for
remote-ssh: host, SSH user, port, an optional path to a private key file, and an optional remoteLIMA_HOME. Setting a profile'slima_homescopes both the remote instance discovery (determining which instanceslimactl listreturns) and file-based state reads (base version stamp, provenance markers). Different profiles can point at the same remote host but use differentLIMA_HOMEpaths, isolating their instances. Forproxmox: host, node, pool, storage, bridge, and a path to a token file — see Proxmox VE for the full field list and the one-time host setup that scopes the API token to a dedicated pool. - An enabled flag.
There is always exactly one Local profile — permanent, created
automatically the first time sand runs, and not deletable (though you can
rename it). Every other profile is remote-ssh or proxmox, and you can
create, edit, enable, disable, or delete as many of those as you like.
profiles.yaml
Profiles are persisted at:
This file is secret-free — it holds a host/user/port/key-path, never a
password or key material — so it's safe to check into a dotfiles repo or copy
between machines: the same profiles.yaml on your laptop and your desktop
gives both clients the same set of named locations. It's also plain,
hand-editable YAML, shaped like this:
version: 1
last_used: a1b2c3d4e5f6a7b8
profiles:
- id: local
name: local
type: local
enabled: true
- id: a1b2c3d4e5f6a7b8
name: work
type: remote-ssh
enabled: true
host: 192.168.1.100
user: debian
port: 22
identity_path: /home/me/.ssh/id_work
lima_home: /home/debian/.lima
id is assigned once at creation and never changes, even across a rename —
it's how sand tracks "the last profile you used" and which managed VMs
belong to which profile without losing track when you rename one. name is
the label you see and address the profile by from the CLI (--profile) and
in the TUI.
lima_home (optional, remote profiles only) is the remote host's LIMA_HOME
directory (where Lima stores instance state). If unset, it defaults to .lima
(Lima's own default, relative to the remote user's home directory). Setting
lima_home affects both discovery and file reads: every limactl command
and provenance marker lookup on the remote host uses this directory. This lets
you isolate instances on a shared host by pointing different profiles at
different LIMA_HOME paths.
Two enabled remote-ssh profiles may not point at the same
user@host:port — sand refuses to save that, so the same physical target
is never double-counted as two separate fleet members. There can also only
ever be one local profile.
Managing profiles
In the TUI
Press p from the board to open the profile management screen: a list of
every profile, its type, and its enabled/disabled state.
| Key | Action |
|---|---|
↑ ↓ |
Move between profiles |
enter |
Edit the selected profile (or create a new one from the ghost row) |
t |
Toggle enabled/disabled |
d |
Delete (not offered for Local) |
When creating a profile (press n), a type picker first asks whether to create
a Remote SSH or a Proxmox profile, then opens the matching form. (Local
is not offered — there is always exactly one Local profile, created for you and
never deletable.) Each form covers every field of its type, so nothing needs
hand-editing afterwards. Whichever field has focus, its description appears
below the form — including, for an optional field, the default it falls back
to when you leave it blank.
Every change here is live — there is no restart, and no separate "apply" step:
- Enabling a profile immediately builds its connection and starts listing its VMs; its tiles appear on the board as soon as that finishes.
- Disabling a profile tears its connection down and shows a banner in the header instead of its tiles, but leaves its managed-VM records and secrets exactly as they were — re-enable it later and everything comes back.
- Deleting a non-Local profile removes it from
profiles.yamland drops its tiles from the board. It never touches the remote host — no VM is stopped or deleted, no SSH command is run against it. Deleting a profile only forgets thatsandshould manage that connection; if you want to re-adopt the same host later, create a new profile pointing at it. - Renaming a profile (a pure name edit, connection unchanged) doesn't rebuild anything — its VMs, jobs, and last-used pointer all follow the rename by ID.
Editing a profile's connection fields (its host, credentials, and the rest), or disabling/deleting it, is only offered while that profile is idle — no build or other job currently running against it — the same guard that protects a single VM from a destructive action mid-build, generalized to the whole profile.
The VM create form also has a profile selector, so you choose which enabled profile a new VM is created on right from the form, without leaving the TUI.
From the CLI
sand create --profile <name> picks which profile a headless create acts
on. sand shell NAME --profile <name> disambiguates when a VM named NAME
exists on more than one enabled profile. See the
CLI Reference for the full flag semantics and resolution
order.
All enabled profiles are active at once
Unlike the old environment-variable selection, which chose exactly one backend per process, every enabled profile is live simultaneously in the TUI: the board shows tiles from this machine, every enabled remote host, and every Proxmox host side by side, each tile labeled with the profile it runs on (see The TUI). A disabled or errored profile shows a banner instead of tiles, naming the profile and the reason.
Because VMs are tracked per-profile, the same VM name can exist under two
different profiles without conflict — claude on your local profile and
claude on your work profile are two independent VMs that happen to share
a display name. sand shell claude is ambiguous in that case and asks you to
disambiguate with --profile.
Requirements on a remote SSH host
These apply to a remote-ssh profile. A Proxmox profile has its own,
different setup — see Proxmox VE Setup.
- Lima (
limactlonPATH) — the remote host runs the exact samelimactlthe local provider does;sandonly changes where it runs. - A working hypervisor (QEMU/KVM, etc.), just like any Linux host running Lima — see the Lima installation docs.
- Passwordless SSH to the target (key-based auth).
sandrunslimactlnon-interactively over SSH, so the connection must not prompt.
What changes when the VM isn't on your machine
Not much. With any profile enabled, sand create --profile, the TUI, sand
shell, and file copy work the way they do locally — the base image is built
on that host once, each VM is a clone of it, and finalize runs there too.
Three things are worth knowing:
- Interactive shells (
sand shell NAMEandSon a tile) wrap the guest tmux attach withssh -tautomatically; detaching still leaves the session running, exactly as with a local VM (see Files and Shells). - File transfer works in both directions, but the route differs. On a
remote-sshprofile the remotelimactl copycan't see your local filesystem, sosandstages the file through the remote host and preserves where it lands in the guest. On aproxmoxprofilesandcopies straight to the guest withscp. - Reaching a web server in the guest differs the same way. On a
remote-sshprofile, Lima forwards the guest's ports to the remote host's loopback, not yours, so they aren't reachable from your machine without a tunnel. On aproxmoxprofile the guest has its own address on your network, so you connect to it directly. See Web Servers and Ports.
Isolation. VMs created on a profile are tagged with that profile's
connection in the managed-VM index, so
they never mix with VMs from another profile — a local limactl list and a
remote profile's list never show each other's instances. The index records
only a profile's user@host:port (or host:node/pool), never a private key,
password, or API token.
Environment variables removed
Earlier (unreleased) builds of sand selected a single remote target for the
whole process via SAND_PROVIDER, SAND_REMOTE_HOST, SAND_REMOTE_USER,
SAND_REMOTE_PORT, SAND_REMOTE_IDENTITY, and SAND_REMOTE_LIMA_HOME
environment variables. That surface has been removed entirely and
replaced by Connection Profiles: set these variables and sand will simply
ignore them. If you were using them, create an equivalent remote-ssh
profile instead (in the TUI's p screen, or by hand-editing
profiles.yaml) — see profiles.yaml above for the field
mapping (SAND_REMOTE_HOST → host, SAND_REMOTE_USER → user,
SAND_REMOTE_PORT → port, SAND_REMOTE_IDENTITY → identity_path,
SAND_REMOTE_LIMA_HOME → lima_home).
More backends later
The three profile types are three implementations of one internal seam in
sand, which is what lets them share every command and screen. Further
backends (DigitalOcean, Linode) can be added the same way; none are available
yet.