
Vm Lab
- 41 installs
- 6.5k repo stars
- Updated August 3, 2026
- steipete/agent-scripts
Helps with ai & agent building tasks during AI-assisted development.
About
vm-lab is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- vm-lab
- AI & Agent Building
- AI-coding skill
Vm Lab by the numbers
- 41 all-time installs (skills.sh)
- +3 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #8,067 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/steipete/agent-scripts --skill vm-labAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 41 |
|---|---|
| repo stars | ★ 6.5k |
| Last updated | August 3, 2026 |
| Repository | steipete/agent-scripts ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
VM Lab
Use this when the task needs a clean macOS VM to test GUI automation, TCC prompts, screenshot capture, clicking, typing, performance, or "two-way validation" of Peekaboo-like tools.
Core idea: run the tool under test inside the guest, but verify it from outside the guest with Parallels screenshots and host-side observations. Do not close apps you do not own.
Safety Rules
- Treat the VM snapshot as disposable, not the host.
- Never print secrets. If
opis needed, follow the 1Password skill and run it only insidetmux. - Prefer fresh app windows you create yourself: TextEdit, a local HTML test page, or a small test app.
- Avoid modifying host state except temporary screenshots under
/tmp. - For git repos inside the VM, use HTTPS remotes and normal branch discipline.
VM Discovery
List VMs:
prlctl list --allGet VM status/IP:
prlctl list --info "macOS Tahoe"Run guest commands as Peter:
prlctl exec "macOS Tahoe" \
'sudo -u steipete -H /bin/zsh -lc '\''source ~/.zprofile 2>/dev/null || true; uname -a'\'''Capture an independent host-side screenshot:
prlctl capture "macOS Tahoe" --file /tmp/vm-reference.png
sips -g pixelWidth -g pixelHeight /tmp/vm-reference.pngTCC / GUI Attribution
For macOS Screen Recording and Accessibility, the responsible process matters.
prlctl execis headless and can fail to produce useful Screen Recording attribution.- Launch the test command from a visible terminal app in the guest when Screen Recording is involved.
- Ghostty works as a GUI terminal if installed.
- After a first failed capture, check
System Settings > Privacy & Security > Screen & System Audio Recording. permissions statusrun throughprlctl execmay still report Screen Recording false after Ghostty is allowed; validate Screen Recording by rerunning the capture from Ghostty.
Open the Screen Recording pane:
prlctl exec "macOS Tahoe" \
'sudo -u steipete -H open "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"'Open Ghostty:
prlctl exec "macOS Tahoe" 'sudo -u steipete -H open -a Ghostty'Running Commands Through Ghostty
Best path: create a guest script with prlctl exec, open/focus Ghostty, then type only a short launcher path into the visible terminal.
Guest script pattern:
prlctl exec "macOS Tahoe" 'sudo -u steipete -H /bin/zsh -lc '\''cat > /tmp/run-vm-lab.zsh <<EOF
#!/bin/zsh
source ~/.zprofile 2>/dev/null || true
cd ~/Projects/Peekaboo || exit 1
Apps/CLI/.build/debug/peekaboo image --path /tmp/peekaboo-vm.png --json
rc=$?
echo "EXIT:$rc"
[ -f /tmp/peekaboo-vm.png ] && sips -g pixelWidth -g pixelHeight /tmp/peekaboo-vm.png
echo "Press return to close..."
read _
exit $rc
EOF
chmod +x /tmp/run-vm-lab.zsh
ln -sf /tmp/run-vm-lab.zsh /tmp/r
open -a Ghostty'\'''Then link the launcher into Ghostty's home directory and type ./r with scripts/parallels_type.py. This avoids unreliable path characters in Parallels key injection.
prlctl exec "macOS Tahoe" \
"sudo -u steipete -H /bin/zsh -lc 'ln -sf /tmp/run-vm-lab.zsh ~/r'"
python3 skills/vm-lab/scripts/parallels_type.py "macOS Tahoe" $'./r\n'Avoid long command typing. Parallels key injection uses its own key-code table and can be layout-sensitive.
Known Pitfalls
- macOS clipboard APIs may fail from
prlctl exec;pbcopy, AppleScript clipboard, and Peekaboo paste can all fail in headless guest context. open -na Ghostty.app --args -e ...may only focus an existing Ghostty window on macOS; do not assume it runs the command.prlctl execmay re-join argv through a guest shell; for complex payloads, pass one fully shell-quoted command string or create the file with a tiny Python writer.- Parallels
send-key-event --keyuses Parallels key values, not macOS virtual key codes. - For normal typing, send
prlctl send-key-event <vm> --key <key>with no--event; explicitpress/releasecan repeat or stick. Return is an exception: use press then release. - Prefer one
prlctl send-key-event --jsonbatch over many separatesend-key-eventprocesses; separate calls can drift under focus/latency. - Use
PRL_KEY_ENTER = 36,PRL_KEY_SLASH = 61,PRL_KEY_R = 27,PRL_KEY_T = 28,PRL_KEY_M = 58,PRL_KEY_P = 33. - If keystrokes produce garbage, send Return to clear the line, create a shorter launcher, then retry.
- If Peekaboo permission probes hang with Screen Recording missing and emit
SWIFT TASK CONTINUATION MISUSE, record it as a product bug; do not confuse it with the VM harness.
Two-Way Validation
For each GUI action, verify through two independent signals:
- Tool-under-test output: JSON, screenshot file, AX result, or app state.
- External verifier:
prlctl capture, host-side image inspection, file content in guest, or process/window state.
Examples:
- Screenshot: compare Peekaboo image dimensions/content against
prlctl capture. - Click: use Peekaboo to click a test button, then verify both guest app state and host screenshot.
- Type: use Peekaboo to type into a controlled text field, then verify AX value and host screenshot.
- Performance: wrap commands with
/usr/bin/time -p; repeat cold/warm runs; keep outputs in/tmp.
Peekaboo VM Baseline
Inside guest:
cd ~/Projects/Peekaboo
git pull --recurse-submodules
swift build --package-path Apps/CLI
Apps/CLI/.build/debug/peekaboo --version
Apps/CLI/.build/debug/peekaboo permissions status --jsonHost-side reference capture:
prlctl capture "macOS Tahoe" --file /tmp/vm-prlctl-reference.pngGuest-side Peekaboo capture through Ghostty:
/tmp/rCompare:
prlctl exec "macOS Tahoe" \
'sudo -u steipete -H /bin/zsh -lc '\''sips -g pixelWidth -g pixelHeight /tmp/peekaboo-vm.png'\'''
sips -g pixelWidth -g pixelHeight /tmp/vm-prlctl-reference.pngReporting
When handing off, include only:
- VM name and OS build.
- repo commit tested.
- permission state.
- commands that passed/failed.
- independent verifier result.
- product bugs discovered.
interface:
display_name: "VM Lab"
short_description: "Verify macOS GUI automation in VM labs"
default_prompt: "Use $vm-lab to run GUI automation under real TCC permissions, verify it with independent host screenshots, and report only actionable results."
#!/usr/bin/env python3
"""Type a short US-keyboard string into a Parallels VM via prlctl.
Use this only for short launchers such as "/tmp/r\n". For long commands,
write a script inside the guest first and type the short path.
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
import time
KEYS = {
"1": 10,
"2": 11,
"3": 12,
"4": 13,
"5": 14,
"6": 15,
"7": 16,
"8": 17,
"9": 18,
"0": 19,
"-": 20,
"q": 24,
"w": 25,
"e": 26,
"r": 27,
"t": 28,
"y": 29,
"u": 30,
"i": 31,
"o": 32,
"p": 33,
"a": 38,
"s": 39,
"d": 40,
"f": 41,
"g": 42,
"h": 43,
"j": 44,
"k": 45,
"l": 46,
"z": 52,
"x": 53,
"c": 54,
"v": 55,
"b": 56,
"n": 57,
"m": 58,
",": 59,
".": 60,
"/": 61,
" ": 65,
"\n": 36,
}
SHIFT_KEYS = {
"_": 20,
":": 47,
'"': 48,
"?": 61,
}
def append_key(events: list[dict[str, int | str | float]], key: int, delay_ms: int) -> None:
if key == 36:
events.append({"key": key, "event": "press", "delay": delay_ms})
events.append({"key": key, "event": "release", "delay": delay_ms})
return
events.append({"key": key, "delay": delay_ms})
def append_shift_key(events: list[dict[str, int | str | float]], key: int, delay_ms: int) -> None:
events.append({"key": 50, "event": "press", "delay": delay_ms})
append_key(events, key, delay_ms)
events.append({"key": 50, "event": "release", "delay": delay_ms})
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("vm", help="Parallels VM name or UUID")
parser.add_argument("text", help="Text to type. Use $'...\\n' from zsh for Return.")
parser.add_argument("--delay", type=float, default=0.08, help="Delay between keys in seconds")
parser.add_argument("--focus-app", help="Guest app to focus first, e.g. Ghostty")
args = parser.parse_args()
if args.focus_app:
subprocess.run(
["prlctl", "exec", args.vm, f"sudo -u steipete -H open -a {args.focus_app!r}"],
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
time.sleep(0.5)
delay_ms = int(args.delay * 1000)
events: list[dict[str, int | str | float]] = []
for char in args.text:
lower = char.lower()
if char in SHIFT_KEYS:
append_shift_key(events, SHIFT_KEYS[char], delay_ms)
elif lower in KEYS and char == lower:
append_key(events, KEYS[char], delay_ms)
elif lower in KEYS and char != lower:
append_shift_key(events, KEYS[lower], delay_ms)
else:
print(f"unsupported character: {char!r}", file=sys.stderr)
return 2
subprocess.run(
["prlctl", "send-key-event", args.vm, "--json"],
input=json.dumps(events).encode(),
check=True,
stdout=subprocess.DEVNULL,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())