Shells and Files
Shells
S on a tile in the Board, and sand shell NAME from the command
line, both attach you to the same thing: the VM's persistent tmux
session inside the guest, prefixed with C-a. They share the same attach
path (internal/lima.AttachArgv), so they are two doors onto one session,
not two different mechanisms.
Because the session is persistent, detaching — with C-a d, or by just
closing your terminal — does not kill what's running in it. Attach
again later with either S or sand shell NAME and it's all still there.
Useful bindings once you're attached (tmux's own, with sand's default
C-a prefix):
| Keys | Action |
|---|---|
C-a d |
Detach (leaves the session running) |
C-a c |
Open a new window |
C-a \| |
Split vertically |
C-a S |
Split horizontally |
A second terminal attaching to the same VM gets its own session grouped against the first, sharing the same windows but tracking its own current one — so two terminals can look at two different windows of the same VM at once.
The VM must be running before you can shell into it; a stopped VM won't
offer S on its tile, and sand shell refuses cleanly with an error
telling you to start it first.
Uploading and downloading files: data, not code
u (upload) and g (download) on a focused tile open a file-transfer pane.
Download is bound to g, not d — d is always delete; see the
Board page for the full keybinding table.
These move data, not code — think a SQL dump going in, or a screenshot, video, or build artifact coming out. They are not a way to get code changes onto or off of a VM; that path is git, and it's what Landing below is for.
- Upload (
u) copies a file or directory from this machine into the guest — for example, seeding a database dump or a fixtures file the guest needs but shouldn't fetch itself. You browse the host for a source starting at your current working directory, then pick a destination directory in the guest. - Download (
g) copies a file or directory out of the guest onto this machine — for example, pulling a screenshot, a recorded video, or a build artifact Claude Code produced, to look at on the host. You browse the guest for a source, then pick a destination on the host.
Both directions require the VM to be running, and neither is offered while a build or a reset is in progress on that VM — starting a transfer against a VM mid-reset would stream files into an instance that's about to be destroyed.
For the flags behind the equivalent CLI subcommands, see the CLI Reference.
Files are one of two things that cross the VM boundary — for the other, reaching a web server listening inside the guest, see Web Servers and Ports.
Pasting Images
v (paste image) on a running VM's tile, and sand paste-image NAME from
the command line, both stage the host clipboard's image on the guest so you
can press Ctrl-V inside Claude Code in the guest to attach it to your message.
The workflow
- Copy an image on your host (screenshot, photo, graphic, etc.).
- In the
sandTUI, pressvon the VM's tile; or from a terminal, runsand paste-image NAME(whereNAMEis the VM's name). - You'll see a status message: "staged image on NAME — press Ctrl-V in the guest".
- In Claude Code inside the guest, press Ctrl-V to attach the image to your message.
The image is held in a single-slot clipboard on the guest, persisting until
you run sand paste-image again (overwriting it with a new image).
How it's secure
The feature is designed to prevent clipboard text from leaking into the guest. sand reads the clipboard image-only on your workstation, verifying an image type is advertised before fetching any bytes. If you have text on your clipboard instead, the command reports "no image on clipboard" and nothing is staged. Inside the guest, the shims that serve the image to Claude Code have no text-serving path at all — image-only by construction.
The image is read on the machine running sand (your workstation), not the
remote host (if you're targeting a remote Lima VM). Only the image bytes
themselves are sent across the network.
Known limitation
A Linux host clipboard holding only a non-PNG image (e.g., JPEG without a PNG variant) is treated as "no image" in v1. macOS always coerces images to PNG, so this edge case applies to Linux hosts only. If it causes real-world friction, it will be revisited.
The verb is only offered on running VMs. Stop or reset a VM and the verb disappears from its tile until the VM runs again.
Landing
l on a focused tile (or sand land NAME [<path>] from the command line)
opens the Landing pane: a listing of that VM's git checkouts, swept live
from the guest, with each one's branch, push state, and PR state. This is
how code — as opposed to the data u/g move above — leaves the VM: not by
copying files, but by pushing a branch and opening a PR against it, exactly
as you would from your own machine.
Each checkout lands in one of a few states, and the pane offers the action that state calls for:
- Pushed, no PR — open a one-shot draft PR for that checkout's pushed branch.
- PR already open — open it in a browser.
- PR state unknown — the branch is pushed, but sand could not confirm
whether a PR exists, because host
ghis unusable or the lookup failed. Opening a draft PR still works (it falls back to the compare URL). - Never pushed, unpushed, or dirty — work that exists only in this VM:
a branch you created in the guest and haven't pushed, commits ahead of the
remote, uncommitted changes, or any combination. Acting on this row
commits and pushes it: sand drops you into the guest with your editor
open on
git commit -a, and pushes the branch when you save (setting its upstream if it has none). Quit the editor without saving and nothing is committed or pushed. - Nothing to land — the checkout is on its repo's default branch with nothing of its own on top. Every fresh clone starts here.
- Local-only — the checkout has no remote configured, so there is
nowhere for Landing to push. This is the only state with nothing to offer.
If such a checkout holds uncommitted or unpushed work, the row still says
so (
local only · 2 uncommitted) — there is nothing sand can do about it, but you should know it is there before deleting the VM.
The commit-and-push action is the only Landing action that runs inside the VM, and it stays there: the commit and the push both happen in the guest, using the guest's own least-privilege push token. No diff, patch, or working tree ever reaches your machine — see Security Model.
Opening a draft PR uses the workstation's own gh — never the guest's
own push token — so the PR is created by you, not by whatever ran inside
the VM. See Security Model for why that
split matters. Without a usable gh on the workstation, Landing falls back to
printing the branch's compare URL instead.
When Landing says gh isn't usable
The pane's header names which mode it's in, and distinguishes two different problems:
gh: not installed— noghon yourPATH.gh: not authenticated—ghis there, butgh auth statusfailed. Rungh auth login, or exportGH_TOKENin the environment you startsandfrom.gh: 1Password did not authorize— you use the 1Passwordghshell plugin (below) and the vault did not hand over the token. Unlock 1Password and reopen the pane.
1Password shell plugin
The 1Password gh shell plugin
is supported directly — no configuration needed. If you have it set up, sand
runs op plugin run -- gh … instead of bare gh, so your token comes from
the vault exactly as it does at your own prompt.
sand detects it by reading ~/.config/op/plugins.sh (the file op plugin init
generates) and checking for op on your PATH. Detection is file-only —
sand never runs op to find out, precisely because that could pop an
authorization prompt underneath the full-screen UI.
Two things worth knowing:
GH_TOKENwins. IfGH_TOKENorGITHUB_TOKENis set in sand's environment, sand uses plainghand ignores the plugin entirely. That is the escape hatch if you would rather sand not touchop.- 1Password may need to authorize. The first Landing action in a while can
require unlocking 1Password. sand gives the
opprocess no terminal, so a prompt can never corrupt the display — but it does mean an authorization that can only be answered in the terminal will time out. Unlock 1Password first and reopen the pane.
Why any of this is needed: sand runs gh directly, never through a shell,
because the branch names and repo slugs it passes come from inside the VM and
must not be able to reach a shell interpreter. The plugin's usual gh alias
is therefore invisible to sand — but op plugin run -- gh … is a plain
command, not a shell trick, so supporting it costs nothing in safety.
The token needs Pull requests: write (fine-grained), or repo /
public_repo (classic) — sand resolves the base branch and the head commit's
message, then POSTs the draft PR. Note this is the workstation's token,
which is a different thing from the token you provisioned into the VM: the
guest's token only ever pushes branches and never needs pull-request
permission at all.
The footer names the action for the checkout you have selected — commit +
push, push, open draft PR, open in browser — rather than a generic
"act", so you can see what enter will do before pressing it. A row with
nothing to do offers no key at all.
The header shows how old the listing is (scanned 2m ago). The rows come from
a background sweep that runs about every 60 seconds, so after committing or
pushing inside the VM's own shell the pane can briefly be out of date. Press
r to rescan immediately.
The pane's own ledger of what it did (which PR it opened, when) is
reopenable later with L (Log), the same key that reopens a build's or
transfer's log.
For sand land's full CLI flags (--pr, --web), see the
CLI Reference.