Send screenshots and files to your remote coding agent over SSH
Docs: https://vmup.dev/docs
Docs • Overview • Getting started • Quickstart • Usage • Commands • Config • Troubleshooting
The npm package is @nyxsky404/vmup. The command is vmup. Full documentation is on the docs site.
A coding agent on a VM reads files by path. Screenshots, PDFs, and recordings sit on your laptop. vmup copies them over SSH into one remote folder, prints a prompt, and copies that prompt to your clipboard. Paste it into the agent thread.
Uploaded 3 files → default
Agent folder: ~/vmup/agents-20260911-140128-a1b2c3d4/
Prompt:
Please inspect all files in ~/vmup/agents-20260911-140128-a1b2c3d4/
Important: You need Node.js 18+ and OpenSSH (
sshon yourPATH). Native Windows is not supported; use WSL.
vmup is a small CLI for one job: collect files on your machine, stage a unique batch, upload it over SSH, and hand back one folder path.
collect → validate → stage → upload → print path + prompt → TTL cleanup
| Piece | Default |
|---|---|
| Config | ~/.config/vmup/config.toml |
| Local staging | ~/.cache/vmup/ |
| Remote root | ~/vmup |
| Batch folder | agents-<YYYYMMDD>-<HHMMSS>-<uuid> |
| Staged names | image-01.png, video-01.mov, file-01.pdf |
| TTL | 5 minutes |
| Per-file size | 200 MB |
| Files per batch | 200 |
| File types | All (set accept_all_files = false for images only) |
| Clipboard after upload | Prompt (clipboard_copy = "prompt"; "path" or "none") |
Transport is your system OpenSSH. Each file is streamed over ssh into a .partial directory, then renamed into place so the agent never sees a half-written folder. On a TTY, the spinner counts bytes as they are written.
- Four collect modes — file args, native picker, clipboard loop, foreground folder watch
- One agent folder — stable
image-NN/video-NN/file-NNnames insideagents-…/ - Profiles and aliases — saved hosts in config, or
--ssh-hostfor a one-shot~/.ssh/configalias - Clipboard-ready output — prompt copied by default; set
clipboard_copytopathornone - JSON for scripts —
--jsonon stdout; spinner stays on stderr - TTL cleanup — remote sweeper every minute, a client-side fallback delete, or
vmup prune --allto wipe now
- Node.js 18 or newer
- OpenSSH client (
ssh) - A remote host you can reach with a key, or an alias in
~/.ssh/config
Confirm both before installing:
node -v
ssh -Vnpm i -g @nyxsky404/vmupAlso:
pnpm add -g @nyxsky404/vmup
yarn global add @nyxsky404/vmup
bun install -g @nyxsky404/vmupOne-shot, no global binary:
npx @nyxsky404/vmup initSame pattern with pnpm dlx, yarn dlx, or bunx.
curl installer (still needs Node 18+). It picks npm, then pnpm, yarn, or bun. Override with VMUP_PM:
curl -fsSL https://raw.githubusercontent.com/nyxsky404/vmup/main/scripts/install.sh | shVMUP_PM=pnpm curl -fsSL https://raw.githubusercontent.com/nyxsky404/vmup/main/scripts/install.sh | shThen:
vmup --versionTip: Newer releases print a two-line notice at most once per day while you stay on an older version. The command does not wait on npm. Disable with
VMUP_NO_UPDATE_CHECK=1. CI (CI=trueorCI=1),--json, non-TTY output, and-Vskip it.
vmup init
vmup check
vmup ./shot.pngvmup init writes ~/.config/vmup/config.toml. Press Enter to keep a value in [brackets]. First run asks for profile name, host, SSH user (ubuntu), key (~/.ssh/id_ed25519), port (22), remote dir (~/vmup), TTL minutes (5), watch folder, whether to accept all file types, and whether to install the remote sweeper.
vmup check tests SSH without the wizard:
SSH OK default ubuntu@192.168.1.10
Remote dir: ~/vmup
Sweeper: installed
After a successful upload, paste into the agent. The clipboard already holds the prompt.
Tip: Non-interactive setup:
VMUP_HOST=… vmup init -y. AddVMUP_USER/VMUP_KEYas needed.--no-sweeperskips remote cron.
If the VM is off during init, config is still saved. You will see SSH setup incomplete. Retry with vmup check, then vmup check --sweeper if cleanup never landed.
Every collect mode ends the same way: one remote folder, a printed prompt, that prompt on your clipboard (human mode). Set clipboard_copy to path or none if you want something else.
vmup shot.png notes.pdf
vmup ~/DownloadsA directory sends immediate children only. Hidden names, .DS_Store, Thumbs.db, and desktop.ini are skipped. Nested trees need the inner folder or the files themselves.
vmupNo arguments and no --clip / --watch opens a GUI picker.
| Platform | Picker |
|---|---|
| macOS | Multi-select (choose file) |
| Linux | zenity (multi) or kdialog (single) |
| Windows | Not supported. Pass paths, or use WSL. |
vmup --clipCopy a file or an image, press Enter to capture, repeat, then type done / stop / q, or Ctrl+D. Ctrl+C cancels (exit 130). Duplicate clipboard bytes are skipped while clip_dedup is true (default).
Note:
--clipreads the OS clipboard, not terminal paste. On macOS, a copied Finder file needs no extra tools; a bitmap (screenshot) needs pngpaste (brew install pngpaste). Linux useswl-pasteorxclip.
vmup watch
vmup watch --dir ~/DesktopForeground only. Files already in the folder at start are ignored. Files added or copied after start are staged (Finder copies included). Press Enter or type stop to upload. Ctrl+C cancels.
Default folder: ~/Desktop on macOS, ~/Pictures/Screenshots on Linux. Only the folder itself is watched, not subfolders.
Do not combine --clip and --watch.
vmup -p work shot.png
vmup --ssh-host lab shot.pngDo not pass --profile and --ssh-host together.
--video and --force only change type filtering when accept_all_files is false. --force never skips SSH checks or size/count limits.
vmup shot.png --ttl 15 --keep-local
vmup shot.png --json| Command | Purpose |
|---|---|
vmup [files…] |
Upload (picker if no files) |
vmup init |
Create or update config; optional remote sweeper |
vmup check |
Test SSH using saved config. --sweeper installs/refreshes cleanup |
vmup profiles |
List saved profiles |
vmup watch |
Same as vmup --watch |
vmup prune |
Delete expired remote batches (--all ignores TTL) |
vmup init -y requires VMUP_HOST. Re-running vmup init interactively adds a profile or overwrites an existing one (current values shown in brackets).
Saved remotes live under [profiles.<name>]. The default profile is used when you pass neither -p nor --ssh-host.
Direct (host + user + key + port):
[profiles.work]
host = "192.168.1.10"
user = "ubuntu"
key = "~/.ssh/id_ed25519"
port = 22
remote_dir = "~/vmup"
ttl_minutes = 5vmup passes -i, -p, BatchMode=yes, and StrictHostKeyChecking=accept-new.
SSH config alias (ssh_host):
[profiles.lab]
ssh_host = "lab"
remote_dir = "~/vmup"
ttl_minutes = 5vmup runs ssh lab …. User, key, and port come from ~/.ssh/config.
--ssh-host lab is the one-shot version (printed profile name ssh:lab). A profile with ssh_host set uses alias mode even if host is also present.
Path: ~/.config/vmup/config.toml
Overrides: VMUP_CONFIG_DIR (directory) → $XDG_CONFIG_HOME/vmup → ~/.config/vmup.
Precedence: flag → env (VMUP_*) → profile → top-level file → built-in default.
default_profile = "default"
prompt_template = "Please inspect all files in {{remote_path}}"
remote_dir = "~/vmup"
ttl_minutes = 5
watch_dir = "/Users/you/Desktop"
watch_include_video = false
accept_all_files = true
max_file_mb = 200
clip_dedup = true
clipboard_copy = "prompt"
[profiles.default]
host = "192.168.1.10"
user = "ubuntu"
key = "~/.ssh/id_ed25519"
port = 22
remote_dir = "~/vmup"
ttl_minutes = 5{{remote_path}} is replaced with the remote folder (trailing / added if missing). After upload, human mode copies the prompt to the clipboard (clipboard_copy = "prompt"). Use "path" for the folder only, or "none" to skip the copy. --json never copies. After you change ttl_minutes, run vmup check --sweeper so the remote script matches. Editing the file alone does not rewrite ~/vmup/.cleanup.sh.
Selected environment variables:
| Variable | Role |
|---|---|
VMUP_HOST / VMUP_USER / VMUP_KEY / VMUP_PORT |
Direct-mode target. VMUP_HOST required for init -y |
VMUP_PROFILE / VMUP_SSH_HOST |
Profile name, or one-shot OpenSSH alias |
VMUP_REMOTE_DIR / VMUP_TTL_MINUTES |
Remote root and TTL |
VMUP_ACCEPT_ALL |
0 images-only, 1 all types |
VMUP_CLIPBOARD_COPY |
prompt (default), path, or none |
VMUP_MAX_FILE_MB |
Per-file size cap |
VMUP_NO_UPDATE_CHECK |
1 skips the notice and the background registry check |
NO_COLOR |
Any value: no ANSI |
Empty string is treated as unset. SSH_AUTH_SOCK allows a missing key in direct mode.
Remote batches expire after TTL minutes (default 5). vmup init installs ~/vmup/.cleanup.sh and a crontab line that runs every minute. The script removes agents-* and agents-*.partial directories older than TTL. It does not wipe the remote root.
vmup prune
vmup prune --all
vmup prune --id agents-20260911-140128-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
vmup prune --local
vmup prune --install-sweepervmup prune --all deletes every remote agents-* batch immediately, even if TTL has not elapsed. --local also deletes staging under ~/.cache/vmup/ older than 24 hours. Successful uploads already delete that cache unless you pass --keep-local. Failed SSH or upload keeps local staging.
Warning: The client-side delete is a detached
sleepthenssh rm. It dies if this machine sleeps or exits. If~/vmup/.cleanup.shis missing, batches may linger. Reinstall withvmup check --sweeper.
vmup shot.png --json
vmup check --jsonJSON goes to stdout. Spinner, skip warnings, and human text stay on stderr. --json does not copy to the clipboard.
{
"ok": true,
"batchId": "agents-20260911-140128-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"profile": "default",
"remotePath": "~/vmup/agents-20260911-140128-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/",
"prompt": "Please inspect all files in ~/vmup/agents-20260911-140128-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/",
"count": 2,
"files": ["image-01.png", "file-01.pdf"],
"sweeper": true
}vmup prune --json (including with --all) does not emit this schema. The flag only hides the update notice.
| Code | Meaning |
|---|---|
0 |
Success. init also returns 0 when config was written even if SSH failed. |
1 |
Bad flags or unusable target |
2 |
Nothing to upload |
3 |
SSH / preflight / sweeper install / prune |
4 |
Upload failed (staging kept) |
130 |
Cancelled during collect (staging deleted) |
| You see | What to do |
|---|---|
Profile "default" has no host |
Run vmup init, or set VMUP_HOST |
SSH key not found |
Fix key in config or VMUP_KEY |
permissions are too open |
chmod 600 on the private key |
Permission denied (publickey) |
User, key, or authorized_keys mismatch |
Connection refused / timed out |
Port, firewall, VPN, VM power |
Could not resolve hostname |
DNS, or the Host name in ~/.ssh/config |
No files to upload (exit 2) |
Path missing, picker cancelled, or watch/clip with zero captures |
SSH preflight failed + Local staging kept |
Fix the host, then retry. Staging stays under ~/.cache/vmup/ |
Use either --profile or --ssh-host |
Pick one |
Use either --all or --id, not both |
Pick one |
| Sweeper missing | vmup check --sweeper |
| Batches still on the VM | vmup prune --all (ignores TTL). One batch: vmup prune --id … |
| Clipboard image ignored (macOS) | brew install pngpaste |
| No GUI picker (Linux) | Install zenity/kdialog, or pass file paths |
vmup check is the retry path that skips the wizard. Preflight timeout is 20 seconds. Upload timeout is 10 minutes per file. --force never skips connectivity checks.
A working ssh lab and a failing vmup -p lab usually means the profile is in direct mode (ubuntu@host) while you expected the alias. vmup profiles prints ssh_host=lab for alias mode.
Docs: https://vmup.dev/docs. Source and issues: github.com/nyxsky404/vmup. How to send a change: CONTRIBUTING.md. Clone, then npm install && npm run build && npm test.