vmsh is an interactive shell for running commands across host, VM, and SSH
systems from one prompt. A normal shell keeps context as "the current working
directory." vmsh extends that idea: the current context is both the selected
system and that system's working directory. Ordinary command lines run in that
context, and @ control lines change the selected system or ask vmsh to do
something directly.
vmsh is a product shell around the ccvm daemon: OCI images become selectable
VM systems, and cc remains the underlying VM runtime, image importer, and
debug command repository.
The repository is intended to be published as github.com/tinyrange/vmsh.
- Runs ordinary shell commands on the host by default.
- Tracks the selected system as part of shell context, alongside the working directory.
- Switches to VM-backed systems with
@<image>and back to the host with@host. - Keeps host and guest shell state warm when possible, so
cd, aliases, functions, and exported variables survive across commands. - Mounts the host root into guests at
/hostand mirrors the current host directory into the guest working directory. - Supports named VMs, memory/CPU sizing, sudo/root execution, networking toggles, and architecture-specific image aliases.
The demo is generated from real vmsh commands with a local VM and local demo
SSH server:
./tools/build.go demoExample interactive session:
@alpine
cat /etc/alpine-release
cd /tmp
printf 'hello\n' > note.txt
@work --from ubuntu:24.04 --memory 2g --cpus 4
python3 --version
@host git status
@alpine --no-network
sh -lc 'uname -m && whoami'
@ --sudo apk add curl- Go 1.25 or newer, matching
go.mod. - Checked-out
ccandgowinsubmodules. - A supported virtualization host when running VM commands:
linux/amd64with KVM and user access to/dev/kvm.windows/amd64orwindows/arm64with Windows Hypervisor Platform enabled.darwin/arm64with Hypervisor.framework.linux/arm64with KVM.
- Network access when downloading kernels or pulling OCI images.
cmd/vmsh: thevmshshell.cc: git submodule containingccvm, VM backends, image import, and the lower-levelccCLI.gowin: git submodule containing the native window, input, and OpenGL frontend used by SquadVM and NeurodeskAppX.docs: focused development notes and test recipes.docs/desktop-automation.md: opt-in, loopback-only framebuffer capture and guest input for the desktop apps.tools/build.go: local build and run helper forcc,ccvm, the native desktop frontends, andvmsh. It uses the checked-outccandgowinsources, signs the native payloads on macOS, and can launch the builtvmsh..github/workflows/release.yml: tag-triggered single-binary releases for Linux, Windows, and signed macOS ARM64.docs/design: accepted plans for cross-cutting vmsh features.
Install the latest release to ~/.local/bin:
curl -fsSL https://raw.githubusercontent.com/tinyrange/vmsh/main/install.sh | shThe installer supports macOS ARM64, Linux ARM64/AMD64, and Windows ARM64/AMD64 release binaries. To install a specific release or choose another destination:
VMSH_VERSION=v0.1.0 VMSH_INSTALL_DIR=/usr/local/bin sh install.shClone with submodules:
git clone --recurse-submodules https://github.com/tinyrange/vmsh.git
cd vmshIf the repository was cloned without submodules:
git submodule update --init --recursiveRun the shell locally:
./tools/build.go runvmsh expects an interactive terminal for normal use. Interactive sessions use
the native vmsh line editor, persistent history stored in the ccvm cache
directory, and autocomplete for @ builtins, cached image names, options,
command names, and host paths.
By default, a vmsh frontend owns its daemon session and cleans it up when the
frontend exits. Start with -system-session or run @detach to keep the
session available after the current frontend closes.
vmshd authenticates local connections, and its state and credential files are restricted to the operating-system account that started it. All vmsh frontend processes running as that account currently share one daemon security principal: they are not an isolation boundary from each other and may discover or control the account's other daemon sessions. Do not run an untrusted vmsh frontend under the same account. Guests, SSH targets, and other clients remain untrusted and must interact with the host only through explicitly granted interfaces.
Frontend-scoped credentials and cross-frontend authorization are tracked in issue #112.
On Windows, the same helper can be run with:
go run .\tools\build.go runRun an existing ccvm binary instead:
(cd cc && go run ./internal/cmd/build-guestinit)
go build -o build/vmsh/vmsh ./cmd/vmsh
./build/vmsh/vmsh -ccvm /path/to/ccvmCheck the local build identity:
vmsh --versionRun a non-interactive script:
./tools/build.go
./build/vmsh/cc -ccvm ./build/vmsh/ccvm pull alpine ./cc/fixtures/alpine.simg
cat > /tmp/vmsh-smoke <<'EOF'
@smoke --from alpine --memory 256 --no-network sh -lc 'whoami; uname -m'
EOF
./build/vmsh/vmsh -ccvm ./build/vmsh/ccvm -script /tmp/vmsh-smokevmsh is a session shell. It treats ordinary lines as commands in the current
context: selected system plus working directory. Lines beginning with @ are
vmsh control lines that switch system context, create named VM systems, run
builtins, or apply one-shot options:
@<oci-image> [vmsh-options] [--] [command...]The primary workflow is selecting a system, then running ordinary commands in that system:
@alpine
uname -a
cat /etc/alpine-release
cd /tmp
pwd
@host
git statusAppending a command to a context line is supported for one-shot use, but it is not the main execution model:
@alpine uname -aCommon forms:
@alpine # select the context and start its VM
uname -a # run in the selected context
@alpine uname -a # one-shot command in alpine
@host # switch back to the host context
@host pwd # one-shot host command
@work --from alpine --memory 4g # create or switch to a named VM system
@ --sudo whoami # run as root in the current VM
@alias ll=@host ls -la # create an alias
@alias expand ll /tmp # preview the expanded command
@jobs # list background jobs
@status # show selected context and VM status
@install # install/update the user-wide vmshd daemon copy
@upgrade # install the latest release and restart vmsh
@version # show vmsh build metadata
@stop work # stop a named VMBuiltins:
@help
@host [command...]
@jobs
@sessions
@detach
@ps
@status
@install
@upgrade
@version
@start
@stop [name|vm:name|ssh:name]
@forward <host-port:guest-port>
@copy SRC DST
@alias [name=value]
@alias expand line@host with no command switches the current system to the host. @host <command> runs a one-shot host command.
Pipelines can mix host, VM, and SSH stages. vmsh follows normal POSIX shell
status semantics: the pipeline status is the final command's status. When an
earlier mixed-context stage exits non-zero, vmsh also prints a diagnostic that
names the stage number, context, exit status, and command so the final stage does
not hide the failure.
Guest commands receive a TTY, terminal dimensions, and terminal color
environment. vmsh keeps command execution non-interactive and adds a small
color prelude for common commands such as ls. Interactive host and guest
commands run through persistent shell sessions when possible, so shell state can
survive across commands. Commands that need full foreground terminal control
fall back to a one-shot shell path.
Copy endpoints use explicit context prefixes so accidental names fail early:
@copy @host:./file.txt @:~/file.txt # host to current context
@copy @:~/file.txt @host:./file.txt # current context to host
@copy @vm:work:/tmp/out @ssh:build:/tmp/out # named VM to SSH host
@copy @image:alpine:/tmp/out @host:./out # image context by name@copy follows normal copy semantics across host, VM, isolated VM, and SSH
endpoints: files overwrite files, existing directory destinations receive the
source by name and merge with existing contents, and directory/non-directory
type conflicts fail instead of replacing the destination. Copy errors include
both source and destination endpoints. Interactive copies show lightweight
progress on the terminal; non-interactive copies stay quiet for scripts. Remote
to remote copies stream through a temporary host staging directory and remove it
when the transfer finishes or fails.
Supported options:
--from <source>
--cwd <guest-path>
--user <user>
--sudo
--memory <n|nM|nG>
--memory-mb <n>
--cpus <n>
--network
--no-network
--nested
--no-nested
--arch <amd64|arm64>Use -- when the guest command itself begins with an option:
@alpine -- --helpAfter selecting a context, ordinary command lines run there:
@obsd-build --from openbsd --memory 4g --cpus 1 --network
pwd
cd /host/path/to/workspaceUse @host ... for one command on the host, or another @<image> ... line to
run a one-off command in a different context.
Guest commands run as UID 1000 by default. Use @ --sudo <cmd> or
@sudo <cmd> to run a command as root in the current VM.
If the daemon reports nested virtualization support, vmsh enables it by
default for VM contexts. Use @ --no-nested to disable it for the current
context or a one-shot command.
Use -record session.cast to write asciinema v2 output. Use
-record-raw session.raw.jsonl to write a lossless JSONL event stream with
base64 terminal input/output bytes and resize events for rendering and
debugging investigations.
Pushing a version tag matching v* runs the release workflow:
git tag v0.1.0
git push origin v0.1.0The workflow builds one standalone vmsh binary per target:
linux/amd64linux/arm64windows/amd64windows/arm64darwin/arm64
Release binaries always include the vmshd daemon entrypoint in the same Go
executable as vmsh. At runtime, vmsh re-execs itself with
VMSH_INTERNAL_VMSHD=1 when it needs to start the authenticated local daemon.
Guest init helpers are built through the cc runtime cache path when a backend
needs them.
The release workflow also supports manual dry runs from GitHub Actions. Use
workflow_dispatch, provide a version string for artifact names, and leave
publish disabled to build, sign, notarize, upload artifacts, and generate
checksums without creating a GitHub Release.
The macOS binary is built on macos-15 and codesigned with the Hypervisor
entitlement from tools/entitlements.xml. Configure these repository secrets
for Developer ID signing and notarization:
MACOS_CERTIFICATE: base64-encoded.p12signing certificate.MACOS_CERTIFICATE_PWD: password for the.p12certificate.MACOS_DEVELOPER_ID: Developer ID Application identity. The workflow also acceptsDEVELOPER_IDfor compatibility with olderccrelease settings.APPLE_ID: Apple ID used bynotarytool.APPLE_ID_PASSWORD: app-specific password fornotarytool, or@keychain:<profile>to use a preconfigured notary keychain profile.TEAM_ID: Apple Developer Team ID.
The workflow signs the binary with hardened runtime, submits a temporary ZIP containing that binary to Apple's notary service, and publishes the single signed binary as the release asset.
