Skip to content
On this page

Install a macOS guest

Restore macOS into an owned local bundle and open its setup console.

Vectis downloads macOS 27.2 (build 26B123) directly from Apple and verifies its pinned SHA-256 and exact size before installation. The selected revision does not change when Apple publishes a new release. You can instead supply an existing Apple macOS 26 or 27 IPSW. A macOS 27 guest needs a host running macOS 27 or later; the Apple virtualization helper checks host and restore-image compatibility. Guest SSH enrollment uses a generated local connection script and explicit guest verification. Xcode is not installed implicitly.

Choose existing image and disposable VM directories. The image volume needs at least 70 GiB free for automatic download and installation, or 40 GiB with an existing IPSW. Stop existing VMs and resolve interrupted runner operations first. The installer uses a new bundle; it never replaces a registered base image.

vectis environment install-macos macos-27-arm64 \
  --image-directory /absolute/path/to/images \
  --storage-path /absolute/path/to/vms \
  --cpu 2 --memory-mib 4096 --disk-gib 64 --json
vectis operation wait <operation-id> --timeout 3600000 --json

For existing media, add --restore-path /absolute/path/to/restore.ipsw. In desktop Environments > Prepare a guest image, select macOS 27 ARM64. Leave the optional restore path empty to download, or choose an existing image. CLI, desktop and MCP all use the same durable service operation. Closing the desktop does not cancel installation.

A completed restore reports action_required, a setupId and the installed bundle path. This means the operating system is installed, not that a runner is ready.

vectis environment open-macos-setup <setup-id> --json

This opens the guest console on the host Mac. Create a local vectis account without Apple ID and enable guest Remote Login. Keep FileVault disabled inside this CI guest so fresh clones can boot unattended. This does not change host encryption. Close the console or cancel its operation to stop the guest. Reopening the same setup continues from its existing disk.

Connect and verify the guest

Keep the setup console open with guest Remote Login enabled. Desktop offers Connect guest SSH on host beside the running setup operation. This opens a generated connection script in Terminal on the host. From the CLI:

vectis environment connect-macos-guest <setup-id> --open-terminal --json

Without --open-terminal, the operation returns the generated scriptPath for a human to open locally. Non-interactive agents can prepare enrollment without launching a password prompt. Remote control opens Terminal on the host, not on the controller machine.

The script uses a dedicated guest key and an isolated known-hosts file. Confirm the guest host-key fingerprint in Terminal, then enter only the guest vectis password there. To inspect the guest’s fingerprint, run ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub inside the guest. The password is never sent to Vectis or the cloud. Do not disable host-key checking or supply a general-purpose host identity.

After the script finishes, choose Verify guest SSH, or run:

vectis environment verify-macos-guest <setup-id> --wait --json

Verification checks the pinned SSH connection, ARM64 architecture, clock synchronization, macOS 26 or 27, guest FileVault being off, and the guest’s developer toolchain. Successful verification still returns action_required: shut down the guest from its Apple menu, then choose Finish macOS setup, or run:

vectis environment finish-macos-setup <setup-id> --wait --json

Vectis registers the environment only after the same verified setup session has stopped. Reopening its console requires another verification before finishing. A registered environment still needs repository linking and a real Actions verification job. Each runner checks guest readiness again before GitHub registration.

Network access

Apple Virtualization NAT does not enforce egress filtering. The guest can reach the internet, the host and the local network. Status reports network: { egress: "open", enforcement: "not_enforced" }, including for existing environments without an explicit setting.

vectis environment configure <id> --egress open --json is accepted. Requesting internet_only fails with egress_unsupported: “Apple Virtualization NAT cannot restrict guest network access yet; this environment can reach the host and local network.” Windows environments support enforced internet-only egress. Inspect effective state with vectis status --json, vectis environment list --json or MCP vectis_status; MCP uses the same configuration command.

What the guest contains

A macOS guest is whatever you install in its setup console. Vectis restores Apple’s image, enrolls SSH and verifies the guest; it cannot install Xcode for you, because Apple does not permit redistributing it and it needs an Apple ID and tens of gigabytes.

One thing is not optional. Without a developer toolchain the guest has no working git, so actions/checkout fails on its first step and almost no workflow gets further. Verification refuses such a guest and tells you what to install. While the setup console is open, either run xcode-select --install for Apple’s Command Line Tools, which bring git, clang, make and the macOS SDK, or install the full Xcode and open it once to accept its license. Jobs that build with Xcode, sign, or need a specific Xcode version need the full install, not the Command Line Tools.

Everything else follows the same rule as the other guests: actions/setup-* actions download toolchains at job time, and anything your jobs always need, such as Homebrew formulas or a specific Ruby, can be installed once into a derived environment with environment.provision, or installed in a workflow step.

Guest TLS trust

In October 2026 the private keys of eight eMudhra intermediate certificate authorities were found to be recoverable. Their chains end at the emSign ECC Root CA - G3 root and, through Sectigo cross-signatures, at AAA Certificate Services. Browsers block these chains, but Apple’s curl and git read /etc/ssl/cert.pem and check neither revocation nor Certificate Transparency. On macOS 27.2 both still accepted the compromised chains on 2026-10-21.

Vectis handles this for runner jobs automatically. Before each runner starts, it writes a copy of /etc/ssl/cert.pem without the two roots into the runner’s private directory and exports it as SSL_CERT_FILE, CURL_CA_BUNDLE and GIT_SSL_CAINFO, which jobs inherit. No sudo is needed and the guest trust store is untouched. If the source is unreadable or the filter cannot be verified, the runner does not start. Tools that ignore those variables and read their own bundle are unaffected by this filtering; tools that use Apple’s system trust depend on Apple’s own block.

The manual step below stays optional, for interactive use inside the guest. Until Apple updates /etc/ssl/cert.pem, remove the two roots from it while the setup console is open, before you finish setup. This changes only the guest, never the host. The Mozilla bundle that curl publishes no longer contains either root.

set -euo pipefail
work=$(mktemp -d)
awk -v dir="$work" '/-----BEGIN CERTIFICATE-----/ { n++; file = sprintf("%s/%04d.crt", dir, n) } file { print > file } /-----END CERTIFICATE-----/ { close(file); file = "" }' /etc/ssl/cert.pem
: > "$work/cert.pem"
for cert in "$work"/*.crt; do
  case "$(/usr/bin/openssl x509 -in "$cert" -noout -fingerprint -sha256 | cut -d= -f2 | tr -d ':')" in
    86A1ECBA089C4A8D3BBE2734C612BA341D813E043CF9E8A862CD5C57A36BBE6B | D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4)
      /usr/bin/openssl x509 -in "$cert" -noout -subject ;;
    *) cat "$cert" >> "$work/cert.pem" ;;
  esac
done
sudo cp /etc/ssl/cert.pem /etc/ssl/cert.pem.before-emudhra
sudo install -m 0644 "$work/cert.pem" /etc/ssl/cert.pem
rm -rf "$work"

The loop prints the two removed subjects. Afterwards curl https://dv-g3b-emudhra.we-have-a-problem.com/ fails with a certificate error, and curl https://github.com/ still succeeds. Apps that use Apple’s system trust instead of this file depend on Apple’s own block.

Interruption and recovery

vectis operation cancel <operation-id> --json
vectis environment resume-macos <setup-id> --json

Interrupted downloads retain their pinned source and partial bytes for the same setup. Completed download bytes are verified again before reuse. After a restore interruption, the service verifies the previous process has stopped. A completed restore is preserved. A failed restore retries into a new bundle and retains interrupted files for inspection; it does not overwrite uncertain disk state. Inspect the private preparation.log in the operation’s image directory for errors.

A setup console is a local interactive operation. Remote callers can request it, but must use the host’s screen to complete guest OS steps. No VNC or guest password is exposed through the cloud.

Discard an unused setup

To permanently remove a stopped, unregistered setup, confirm its exact environment ID:

vectis environment discard-macos <setup-id> --environment <environment-id> --json

Desktop offers the same confirmation beside the setup operation. This deletes the current attempt’s owned image directory, including its guest disk and setup logs. It preserves downloaded or supplied source IPSWs, registered environments, and older interrupted attempt directories. Active or registered setups and directories without matching ownership metadata cannot be discarded.

Provision an existing guest

Use environment provisioning to create a new base.bundle from a ready macOS environment. The runner is installed without sudo under $HOME/.vectis-runner/<version>, and optional setup scripts run as the SSH user with Bash. Provisioning does not install Xcode or change the parent bundle.

Provisioning uses this bounded shutdown sequence instead of the virtual power button, which can open a confirmation dialog in a logged-in macOS session:

  1. Over SSH as the guest user, run osascript -e 'tell application "loginwindow" to «event aevtshut»', then wait up to 60 seconds for the VM to stop. Apple’s SDK identifies shut as kAEShutDown; rsdn is kAEShowShutdownDialog and would request confirmation. The command does not invoke sudo. Apple documents the loginwindow shutdown event, but on a cold-booted clone with no logged-in console session (root-owned /dev/console) the AppleEvent was observed to fail with -10810, and provisioning then uses the fallbacks below. Guest applications can also cancel shutdown.
  2. If the VM is still running and sudo -n true succeeds, run sudo -n /sbin/shutdown -h now and wait up to another 60 seconds. SSH enrollment does not grant passwordless sudo, and provisioning never prompts for a password.
  3. If the guest remains running, complete two separate sync commands over SSH, wait 5 seconds, then force-stop the owned provisioning VM through the Apple helper. Each SSH command and helper stop has a 30-second limit. A failed or timed-out sync prevents image registration; failed or cancelled provisioning removes its partial output after confirmed VM cleanup. Unconfirmed cleanup requires inspection.

The final operation result reports shutdown as loginwindow, sudo, or forced_stop_after_sync. The last value explicitly means the image was frozen after a forced stop following two successful syncs, not after an observed clean guest shutdown. This fallback applies only to the derived provisioning image, never ordinary runner VMs or user data. Inspect it with vectis operation get <operation-id> --json.