
Moving a hosted Mac agent is only risky in the seam between "old machine drained" and "new machine accepting work". The procedure below closes that seam with an explicit drain, an export that excludes secret values, a fresh runner registration, and a hold window for rollback.
Recommendation. If the agent is an Intel-compatible CI runner, API poller, or launchd-driven job, the Figment virtual Intel macOS plan at $29/month (2 vCPU, 8 GiB RAM, 80 GiB disk; browser-console access; VM capacity is being verified, so request early) is the cheapest valid target. If the job needs ARM execution, Apple-silicon-only binaries, Xcode 27, a GPU, or audio, request an M-series Mac starting at $95/month — the exact chip, RAM and disk are confirmed in your quote. Teams that want the CI setup, backups, monitoring and 30 minutes of monthly support handled for them should price the $249 Managed Build Mac. The i3 Mac at $49 and physical M-series plans collect requests until supply is confirmed; start the request a few days before your maintenance window.
| Scenario | Plan | Monthly |
|---|---|---|
| Like-for-like Intel toolchain, launchd agents, scripts | Virtual Intel macOS | $29 |
| Physical Intel box for local I/O (request-based) | i3 Mac | $49 |
| ARM execution, Xcode 27, GPU or audio | M-series Mac | from $95 |
| Delegated CI setup, backups, monitoring, 30 min support | Managed Build Mac | $249 |
| Two M-series + two runners, team setup, 1 h support | Managed Mac Team | $999 |
The $29 VM runs Intel macOS on Linux/KVM, with no GPU acceleration and no audio. It can run compatible Intel Mac toolchains, including Xcode 26 or earlier on Intel, and it can cross-compile and sign supported arm64 device apps and universal macOS binaries. It cannot execute arm64-only binaries, Xcode 27, or visionOS tooling — pick M-series for those.
If your job genuinely needs Apple silicon, compare the entry price honestly. CloudMac lists M4/16 GB/256 GB at $99.99/month, and cloudmac.org advertises a $999/year option — that is $83.25 effective monthly, roughly $141 less per year than twelve months of Figment's $95 M-series entry. Two caveats: Figment's $95 is an unspecified M-series starting price, not a declared M4/16 GB spec (the exact hardware is set in your quote), and CloudMac's 512 GB and 1 TB rows are currently marked "Fully reserved, Next allocation not scheduled". If Apple silicon is a nice-to-have and the Intel toolchain is fine, the $29 VM remains the cheapest valid target by a wide margin.
Treat the agent as three separable things and move each differently:
| Category | Contents | Transfer method |
|---|---|---|
| Configuration | launchd plists, scripts, cron entries, environment variable names | Source control or secure file transfer |
| Secrets | API keys, tokens, signing identity, Developer ID private keys | Retrieve fresh from your secret manager on the new host; rotate at source |
| State | Job state files, checkpoints, artifact cache | Copy explicitly and validate against a known-good reference |
Never dump the live environment to a plaintext file — printenv output contains anything your shell has loaded. Export the list of variable names your agent requires, then re-inject values from your CI or password manager on the new host. Signing identities belong in an encrypted p12 (private key included) handled by your secrets system, not copied out of Keychain as bare files. Provisioning profiles are regular files on disk and should be re-downloaded from Apple Developer, not "exported" from Keychain.
You will need: admin on your CI service to issue a new runner registration token and remove the old runner; access to your secret manager to retrieve and rotate credentials; and permission to update your own firewall, artifact store or CI allowlists if any of them pin the old machine's egress IP.
touch ~/agent/.drain # your wrapper refuses new jobs if present
tail -F /tmp/agentjob.log # watch in-flight work finish
launchctl list | grep com.yourteam.agentjob # PID column shows "-" when idle
Mark the runner offline in your CI UI and tell upstream schedulers (cron orchestrators, webhook dispatchers) that this host is going away.
#!/usr/bin/env bash
set -euo pipefail
umask 077
OUT="$HOME/agent-export-$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$OUT"/{launchd,scripts,state,env-names}
cp ~/Library/LaunchAgents/com.yourteam.agentjob.plist "$OUT/launchd/"
cp /usr/local/bin/run-agent-job.sh /usr/local/bin/reconcile-job.sh "$OUT/scripts/"
# State you intentionally carry across
[ -f ~/agent/job.state ] && cp ~/agent/job.state "$OUT/state/"
[ -d ~/agent/checkpoint-latest ] && cp -R ~/agent/checkpoint-latest "$OUT/state/"
# NAMES ONLY — values stay in your secret manager
grep -oE '\$\{?[A-Z_][A-Z0-9_]*' /usr/local/bin/run-agent-job.sh \
| tr -d '${' | sort -u > "$OUT/env-names/required.txt"
brew bundle dump --file="$OUT/Brewfile" --describe --force
echo "Wrote $OUT. Transfer over an encrypted channel."
This script is an illustrative example, not a benchmark — adapt variable discovery to how your scripts actually reference secrets.
mkdir -p ~/agent && touch ~/agent/.drain # start blocked
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew bundle install --file=/path/to/agent-export/Brewfile
install -m 0755 /path/to/agent-export/scripts/*.sh /usr/local/bin/
cp /path/to/agent-export/launchd/*.plist ~/Library/LaunchAgents/
# Inject secrets from your manager — one per required.txt line
# e.g. export API_KEY="$(op read 'op://CI/agent/api-key')"
cp /path/to/agent-export/state/job.state ~/agent/
For a GitHub Actions self-hosted runner, follow the current docs on removing runners and hardening self-hosted runners.
On the old machine (or via the repository/organization Settings → Actions → Runners UI):
./config.sh remove --token OLD_REMOVAL_TOKEN
On the new machine, with a fresh registration token:
./config.sh --url https://github.com/YOUR_ORG/YOUR_REPO \
--token NEW_REGISTRATION_TOKEN \
--name figment-mac-agent-v2 \
--labels mac,automation
./svc.sh install && ./svc.sh start
GitHub automatically removes a persistent self-hosted runner that has not connected for more than 14 days, and an ephemeral runner after 1 day — so the explicit removal step above is a cleanup courtesy if you are inside those windows, and required if you are outside them.
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.yourteam.agentjob.plist
shortcuts run "AgentSmokeTest" # see support.apple.com shortcuts CLI guide
# Confirm runner shows Online in the CI UI, then:
rm -f ~/agent/.drain
Redirect upstream triggers (cron, webhooks, dispatcher config) to the new host. Keep the old machine drained but reachable for 24–48 hours for rollback, then decommission.
EXPORT
[ ] Drain flag set; in-flight job finished; log quiet
[ ] CI runner shows Offline in service UI
[ ] Upstream schedulers / webhook senders notified
[ ] Export directory contains launchd, scripts, state, Brewfile, required.txt
[ ] No secret values present (grep export dir for known key prefixes)
NEW MACHINE
[ ] Homebrew installed; brew bundle check exits 0
[ ] Scripts installed with mode 0755
[ ] launchd plists present under ~/Library/LaunchAgents
[ ] Secrets pulled fresh from secret manager AND rotated at source
[ ] job.state restored and matches last good checkpoint
REGISTRATION
[ ] Old runner removed (or within auto-removal window)
[ ] New runner registered with fresh token and expected labels
[ ] New runner shows Online before traffic shift
CUTOVER
[ ] Smoke test passed end to end
[ ] Drain flag removed on new machine
[ ] Upstream triggers redirected
[ ] Old machine drained, reachable, monitored 24–48 h
DECOMMISSION
[ ] No rollback events during hold
[ ] Old launchd agents bootout'd; service uninstalled
[ ] Plan cancelled or repurposed with Figment
| Failure | Symptom | Recovery |
|---|---|---|
| Missing secret on new host | Auth error on first real job | Pull from secret manager; never copy from old host |
| Tool version drift | Unexpected output, crash | brew list --versions <formula>; pin and reinstall |
| Runner stays offline | No jobs dispatched after cutover | Check ./svc.sh status and launchctl print gui/$(id -u)/... |
| State divergence | New host replays or skips work | Compare job.state with upstream; replay only verified clean checkpoints |
| Hard failure on new host | Jobs failing within hold window | Reinstate old machine: rm ~/agent/.drain, re-point triggers |
The $29 virtual Intel macOS plan is billed monthly — a two-day or two-week overlap still costs a full month on each host. Schedule the cutover near the start of a billing cycle so the second month buys the most useful validation time. For i3 and M-series plans the overlap terms sit in your quote; confirm them before you start the move. There is no advertised trial or pro-rated daily rate to lean on.
Request your new Figment Mac now and keep the old one running until the hold window closes.
Start a hosted Mac at Figment → · Browse Mac hosting plans →
Related guides: