vm - an automation cli
CLI to drive test VMs — guest control over SSH (paramiko) + host control over VMware (vmrun) —
built for AI agents, so an LLM can drive a VM repeatably for product testing.
One entrypoint: vm [--vm NAME] <verb> .... Success = one status line; errors go to stderr with a
real message and a propagated exit code. (Package vm-automation-cli, import vm_cli.)
Setup
pip install . # installs the `vm` and `vm-init` commands
vm-init # pick your VMs; writes config + folders
vm vm doctor # all checks should PASS
vm-init finds the VMs VMware knows about, auto-detects each guest's OS and IP, prompts for
user/password/snapshot, and writes vmconfig.json plus staging/ and provision/ here. Re-run it
to add VMs (it merges); --agents also drops a "Test VM" section into ./AGENTS.md. To configure
by hand, copy vmconfig.example.json instead. From source, without installing:
python -m vm_cli.cli <verb> / python -m vm_cli.init.
Config lookup: --config PATH, else $VM_CONFIG, else vmconfig.json in the working directory —
so vm finds the config (and provision/) in whatever project you run it from. It holds passwords
and is gitignored. default_vm is used when --vm is omitted.
To wire this into Claude Code or another coding agent, see INSTALL.md.
Verbs
Guest (SSH):
| verb | args | purpose |
|---|---|---|
run |
"<cmd>" [--as USER] [--timeout N] |
exec cmd, print stdout/stderr, exit=remote rc |
push |
<src>... [dest] |
upload files/dirs/globs, keeping relative paths (SFTP, auto base64 fallback) |
pull |
<remote> [local] |
download (SFTP, auto base64 fallback) |
sync |
<localdir> [remotedir] |
bulk push a staging dir |
build-run |
<local-src> [--as USER] [--dir REMOTE] [--keep] [--args ...] |
push+compile(gcc)+run source, one call |
snap |
<path> |
print baseline line inode mtime size sha256 |
verify |
<path> --baseline "<line>" [--token STR] |
print CREATED|MODIFIED|UNCHANGED + token check |
waitfile |
<path> [--timeout N] |
block until file appears (pre-existing file is deleted first, then watched for recreation) |
Host (vmrun):
| verb | args | purpose |
|---|---|---|
vm snapshot |
[SNAP] |
take snapshot (default: config's clean name) |
vm revert |
[SNAP] |
revert to snapshot |
vm reset |
revert to clean + power on + wait for SSH + refresh IP in config | |
vm start / vm stop |
power on/off | |
vm list / vm snapshots |
list running VMs / this VM's snapshots | |
vm ip |
[--save] |
discover guest IP, optionally write to config |
vm doctor |
health check: config, vmrun, vmx, SSH; per user: --as works + sudo rights match config's sudo flag |
|
vm setup-ssh |
(Windows guest) enable OpenSSH Server over VMware Tools; idempotent | |
vm provision |
[--force] |
stage provision/<vm|os>/ into the guest tools dir; run its setup script |
Docs (needs no config): vm docs prints this README from anywhere — it ships inside the
package, so an agent can read it with no repo checked out. --skill prints the vm-recovery
skill, --install-skill [DIR] writes it to DIR/.claude/skills/vm-recovery/, --path prints
the file's location.
Optional (WSL): mount / umount sshfs live-bind the VM's staging_remote into WSL at
$HOME/vmstaging_<host> (under your WSL home, not /mnt, so no root needed). mount creates
staging_remote on the guest first, so it works before your first push. Uses the VM's
wsl_distro, else your default distro; needs sshfs there. The mount is usable only from inside
WSL — it can't be bound to your Windows-side staging\ (FUSE won't mount over DrvFs) and
\\wsl$\... returns Access denied on it. For a Windows drive letter, use sshfs-win/WinFsp.
Provisioning (staging tools into the guest)
Drop files into provision/<vm-name>/ (or provision/<os>/) beside vmconfig.json. On the first
guest command they sync to the guest tools dir (tools_remote, default <home>/tools), are made
executable on Linux, and that dir is prepended to PATH for run — so provision/myvm/strace
makes run "strace -V" work. No manifest; the folder is the config. See provision/README.md.
- Setup hook: an optional
setup.sh/setup.ps1at the folder root runs once after the copy.setup.ps1runs elevated over VMware Tools — where an MSVC/Build Tools installer belongs, since SSH can't elevate. - Idempotent: a hash marker (
<tools>/.provisioned) skips unchanged folders; edit the folder and the next command restages. Force withvm provision --force. - Bake it in: provision once, then
vm snapshot clean, so everyvm resetrestores a fully-loaded guest for free.
Windows guests
Set "os": "windows" on the VM block (see mywinvm in vmconfig.example.json). Windows has no
SSH by default; vm enables it over VMware Tools automatically the first time an SSH verb can't
connect (enabling OpenSSH in the Windows guest...), or on demand via vm setup-ssh. Needs VMware
Tools running and a local admin default_user (the modern.ie VMs' IEUser / Passw0rd!
qualifies). Use forward slashes in remote paths (C:/Users/IEUser/staging); run executes in
cmd.exe. push / pull / sync / run work — build-run, --as, and sudo are Linux-only.
Exit codes
Remote command's rc passes through for run / build-run; 124 = timeout;
125 = can't connect / config error; 0 = success (other verbs).
If a command fails, do this
| symptom | action |
|---|---|
exit 125 (can't connect) |
vm vm ip --save then retry once; still failing → vm vm doctor |
exit 124 (timeout) |
retry with a bigger --timeout N; if it repeats, the command is hanging — report it |
nonzero rc from run/build-run |
that is the remote command's own exit code — read the printed stderr |
vm doctor shows a [FAIL] |
fix that one line (config value, vmrun path, credentials); don't retry other verbs first |
| VM is broken / reverted | use the vm-recovery skill (or: vm reset, then re-sync) |
Do not retry the same failing command more than twice.
Transferring files (push)
push takes files, directories, and globs — expanded by the tool itself, so push docs/*.txt
works whether or not your shell expands it (PowerShell/cmd don't). Each source keeps its relative
path under the destination (default: staging_remote), so a docs/ prefix is recreated:
vm push report.txt # -> <staging>/report.txt
vm push docs/*.txt # -> <staging>/docs/*.txt (docs/ created; .txt only)
vm push src/**/*.py # recursive glob
vm push docs # -> <staging>/docs/... (whole dir, recursive)
vm push a.txt b.txt /home/user/in # 2+ args: trailing non-local arg is an explicit remote dir
vm push local.txt /home/user/x.txt # single file + explicit path = literal rename
Absolute / drive-qualified / .. sources fall back to their basename, so no host layout leaks into
the destination. A non-matching glob or missing file is an error (exit 125).
build-run
Builds in a fresh /tmp/vmbuild.XXXXXX (unique per run, so concurrent agents don't collide) and
removes it afterward; --keep leaves it (path printed to stderr). --dir REMOTE builds into a
chosen dir and leaves source + binary there, named after the source stem (widget.c → widget).
The program runs with the build dir as its cwd. A single --args value is split shell-style
(--args "1 2 3" = three arguments); multiple values pass through literally.
Other behaviors worth knowing
waitfiledeletes a pre-existing target. It means "wait until the job creates this file", so a stale file from an earlier run is deleted first, then watched for recreation — start your job and call waitfile in either order.snap/verifyare stateless.snaponly prints a baseline line; pass it back viaverify <path> --baseline "<line>". No shared state file, so concurrent agents are safe.- Git Bash / MSYS path gotcha. An absolute POSIX remote path in any verb that takes one
(
push,pull,snap,verify,waitfile,build-run --dir) is silently rewritten to a Windows path beforevmsees it —snapthen printsMISSINGfor a file that exists. Local paths are the mirror image: useC:\Users\..., not/c/Users/.... Prefix withMSYS_NO_PATHCONV=1, or use PowerShell (unaffected).
Examples
vm run "id" --as admin
vm run "head -c 64 /etc/shadow" --as admin # permission/read check; no special verb needed
vm build-run ./test.c --args "1 2 3"
vm build-run ./test.c --dir /home/user/build # leaves source + binary there
vm push ./out.bin /tmp/out.bin && vm run "wc -c /tmp/out.bin"
vm push ./a.c ./b.c /home/user/src/ # multiple files in one call
vm vm reset
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vm_automation_cli-0.1.2.tar.gz.
File metadata
- Download URL: vm_automation_cli-0.1.2.tar.gz
- Upload date:
- Size: 40.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f7eb799e0601eba338d63170b6b1b205cac0f401d0890650eb354a41b33cf332
|
|
| MD5 |
960250669970bcfa62c9ada251eea5f0
|
|
| BLAKE2b-256 |
89c18abd5ada9c39615bf257e8393a940d7893a3ad9ab83714da1041d9baf614
|
Provenance
The following attestation bundles were made for vm_automation_cli-0.1.2.tar.gz:
Publisher:
ci.yml on unimpossible/vm_automation
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vm_automation_cli-0.1.2.tar.gz -
Subject digest:
f7eb799e0601eba338d63170b6b1b205cac0f401d0890650eb354a41b33cf332 - Sigstore transparency entry: 2329093582
- Sigstore integration time:
-
Permalink:
unimpossible/vm_automation@b9d60bfcb98600c4f01f4637d0142f43e3ffa4f2 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/unimpossible
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@b9d60bfcb98600c4f01f4637d0142f43e3ffa4f2 -
Trigger Event:
push
-
Statement type:
File details
Details for the file vm_automation_cli-0.1.2-py3-none-any.whl.
File metadata
- Download URL: vm_automation_cli-0.1.2-py3-none-any.whl
- Upload date:
- Size: 34.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aaa70407f8a5efcdf2118859b3f0a17b1aa5d32b93ee386293a0d27674703d59
|
|
| MD5 |
4f34b8ed95d04cce4de5dcdd6dd420e0
|
|
| BLAKE2b-256 |
355c697f49253d1ff78c0b6d5165ef0afa3fd927dacde24c84ef545abec96ff1
|
Provenance
The following attestation bundles were made for vm_automation_cli-0.1.2-py3-none-any.whl:
Publisher:
ci.yml on unimpossible/vm_automation
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vm_automation_cli-0.1.2-py3-none-any.whl -
Subject digest:
aaa70407f8a5efcdf2118859b3f0a17b1aa5d32b93ee386293a0d27674703d59 - Sigstore transparency entry: 2329093637
- Sigstore integration time:
-
Permalink:
unimpossible/vm_automation@b9d60bfcb98600c4f01f4637d0142f43e3ffa4f2 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/unimpossible
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@b9d60bfcb98600c4f01f4637d0142f43e3ffa4f2 -
Trigger Event:
push
-
Statement type: