
Disk pressure is a silent automation killer. A download fills the drive, df returns 100%, and the next write fails with no stack trace in the job log — just a stalled process and a confused on-call engineer. On a hosted Mac with a fixed disk, the right response is a written storage budget, a cleanup allowlist that explicitly names what is safe to delete, and a scheduled check that fires before the disk is actually full.
Figment's $29/month virtual Intel macOS environment ships with 2 vCPU, 8 GiB RAM and an 80 GiB disk. That is enough for most automation that processes documents, calls APIs, or scrapes moderate volumes. If your jobs download large media, cache multi-gigabyte artifacts, or keep rolling browser profiles, 80 GiB fills quickly — especially once system logs and build caches accumulate between runs.
The VM is Intel, runs on Linux/KVM, and has no GPU acceleration or audio. It can run compatible Intel macOS toolchains, including Xcode 26 or earlier for cross-compiling supported targets. If you need Apple silicon execution, a larger disk, or more RAM, request an M-series Mac from $95/month and confirm the exact chip, RAM, and disk in your quote. Physical plans and i3 are request-based until supply is confirmed.
Start a configuration at /mac-hosting/start.
Before writing any cleanup code, list who writes to disk and classify each path. A short table beats a shell wildcard that eats project data by accident.
| Directory | Typical size | Safe to auto-delete? |
|---|---|---|
~/Downloads/ | Varies | Yes, after the job validates each download |
~/Library/Caches/ | 2–10 GiB | Yes, caches rebuild on demand |
/Library/Logs/, ~/Library/Logs/ | 500 MB–2 GiB | Yes, truncate or rotate |
~/Library/Developer/Xcode/DerivedData/ | 10–40 GiB | Yes, rebuilds on next build |
~/workdir/ (your project root) | Depends on project | No — protect this |
~/workdir/checkpoints/ | Small | No — required for crash resume |
~/workdir/artifacts/ | Varies | Only after upload is confirmed |
The script cleans from this allowlist only. It never traverses the whole filesystem looking for large files, because that risks deleting project data, provisioning profiles, or keychain material. It runs as your agent user and never needs root for these paths.
Save as /usr/local/bin/disk-budget-check and make it executable with chmod 755.
#!/usr/bin/env bash
# Checks free space, logs usage, and cleans allowlisted paths when
# free space falls below the warning or critical thresholds.
set -euo pipefail
WARN_GIB=15 # Alert at this many GiB free
CRITICAL_GIB=5 # Clean at this many GiB free
LOG="$HOME/logs/disk-budget.log"
mkdir -p "$(dirname "$LOG")"
free_gib() { df -g "$HOME" | awk 'NR==2 {print $4}'; }
log() { echo "$(date -u +%FT%TZ) $*" | tee -a "$LOG"; }
FREE=$(free_gib)
log "Free: ${FREE} GiB"
if (( FREE >= WARN_GIB )); then
log "OK: above warning threshold."
exit 0
fi
log "WARNING: ${FREE} GiB free is below ${WARN_GIB} GiB."
log "--- Top consumers ---"
du -shx \
"$HOME/Downloads" \
"$HOME/Library/Caches" \
"$HOME/Library/Logs" \
"$HOME/Library/Developer/Xcode/DerivedData" \
2>/dev/null | sort -h | tee -a "$LOG"
if (( FREE >= CRITICAL_GIB )); then
log "Above critical threshold; alerting only."
exit 1 # Non-zero so a monitoring wrapper can send a notification
fi
log "CRITICAL: ${FREE} GiB free. Running allowlist cleanup."
cleanup_path() {
local path="$1"
[[ -d "$path" ]] || return 0
log "Cleaning: $path"
find "${path:?}" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
}
cleanup_path "$HOME/Downloads"
cleanup_path "$HOME/Library/Caches"
cleanup_path "$HOME/Library/Logs"
cleanup_path "$HOME/Library/Developer/Xcode/DerivedData"
FREE_AFTER=$(free_gib)
log "Free after cleanup: ${FREE_AFTER} GiB"
if (( FREE_AFTER < CRITICAL_GIB )); then
log "FAIL: still below critical after cleanup; needs manual review."
exit 2
fi
log "Cleanup complete."
find … -mindepth 1 -maxdepth 1 -exec rm -rf {} + removes only direct children of the named directory and handles empty directories cleanly. The ${path:?} expansion aborts the script if the variable is ever empty, which is the belt-and-braces guard against deleting at /. Review it against your own project layout before running — this is an example, not a reported benchmark.
Agent property-list files live in ~/Library/LaunchAgents/. Save this as com.yourorg.disk-budget.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.yourorg.disk-budget</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/disk-budget-check</string>
</array>
<key>StartInterval</key>
<integer>1800</integer>
<key>StandardOutPath</key>
<string>/Users/agent/logs/disk-budget.out.log</string>
<key>StandardErrorPath</key>
<string>/Users/agent/logs/disk-budget.err.log</string>
</dict>
</plist>
Load it with launchctl load ~/Library/LaunchAgents/com.yourorg.disk-budget.plist. StartInterval is documented in Apple's Creating Launchd Jobs reference, and the agent/daemon distinction and session behavior are in Technical Note TN2083. On the $29 VM you reach the shell through the browser console terminal; there is no inbound SSH, so run launchctl load interactively from that terminal.
Three checks are enough to trust the schedule:
WARN_GIB=1 CRITICAL_GIB=0 /usr/local/bin/disk-budget-check. Expect an OK line.~/Downloads/test-cleanup/ and ~/workdir/important-data/, then run with very high thresholds so cleanup always fires. Confirm ~/workdir/important-data/ is untouched. This is the one test you cannot skip.launchctl list | grep disk-budget should show the label with PID - and last exit 0. Force a run with launchctl start com.yourorg.disk-budget and tail the log.After large jobs, call df -h ~ from the browser console terminal before you queue the next one. If the disk sits under 15 GiB free for several checks in a row, that is the signal to resize — not a cue to raise CRITICAL_GIB.
The 80 GiB disk on the $29 VM is a fixed allocation; there is no on-demand volume expansion in the current offer. Worked example: a nightly job that downloads 6 GiB, writes 2 GiB of derived data, and leaves 1 GiB of logs touches 9 GiB per run. Starting from 75 GiB of usable headroom (80 GiB disk minus the 5 GiB critical reserve), that is 75 / 9 ≈ 8 runs before you cross the threshold. With the allowlist above, each run returns to roughly the same free-space baseline and the plan holds.
| Plan | Disk | Fits this job when |
|---|---|---|
| Virtual Intel macOS — $29/mo | 80 GiB (confirmed) | Light-to-medium automation, API calls, document processing |
| M-series Mac — from $95/mo | Confirm in quote | Larger working set, ARM execution, or more RAM |
| Managed Build Mac — $249/mo | Confirm in quote | One M-series Mac with CI setup, backups, monitoring, and 30 minutes monthly support |
Managed Build Mac adds backups and monitoring on top of a single M-series Mac, which pairs well with the in-process check above: the external monitor catches a stuck machine, the launchd job catches a stuck disk. Pricing, chip, RAM, and disk for physical M-series are confirmed in the quote.
Request a configuration at Figment Mac Hosting and walk through /mac-hosting/start. Once the Mac is ready:
/usr/local/bin/disk-budget-check and chmod 755 it.~/Library/LaunchAgents/ and launchctl load it.To migrate an existing workflow, run du -shx ~/* 2>/dev/null | sort -h on the old host and confirm the allowlist covers the same categories before cutover. Caches on the new host start empty, which is the right moment to prove the allowlist is complete.
Related guides: