Run most of your builds locally
Self-hosted GitHub Actions runners for your Mac
GitHub charges $0.062/minute for their cheapest macos runners. A 20-minute build costs $1.24. Push twice a day and you're spending over $50/month on CI. Run the same jobs on your MacBook and they finish in less than half the time for $0.
Here's what some open source projects would save:
| Project | Builds/mo | Runners | p90 | Cost/mo |
|---|---|---|---|---|
| Alamofire | ~9 | macos-15 | 8m | $14 |
| mattermost-mobile | ~225 | macos-15-large | 25m | $321 |
| SwiftFormat | ~72 | macos-15 | 4m | $22 |
Pricing as of January 2026. Costs calculated from jobs via generate-benchmarks.sh.
Local builds are also faster. Based on XcodeBenchmark:
| Runner | Time |
|---|---|
| GitHub macos-latest M1 x3 ($0.06/m) | 838s |
| GitHub macos-15-large Intel x12 ($0.08/m) | 955s |
| GitHub macos-15-xlarge M2 Pro x5 ($0.10/m) | 339s |
| MacBook Air M2 x8 (2022) | 202s |
| MacBook Pro M4 Max x16 (2024) | 77s |
Features:
- Automatic fallback — workflows detect when your Mac is available; fall back to hosted runners when it's not
- One-click setup — no terminal commands, no manually generating registration tokens
- Lid-close protection — close your laptop without killing in-progress jobs
- Multi-runner parallelism — run 1-8 concurrent jobs
- Network isolation — runner traffic is proxied through an allowlist (GitHub, npm, PyPI, etc.)
- Filesystem sandboxing — runner processes can only write to their working directory
- Resource-aware scheduling — automatically pause runners when on battery or during video calls
localmost is a macOS app that manages GitHub's official actions-runner binary. It handles authentication, registration, runner process lifecycle, and automatic fallback — the tedious parts of self-hosted runners.
Security note: Running CI jobs on your local machine has inherent risks—especially for public repos that accept external contributions. localmost sandboxes runner processes and restricts network access, but these are not VM-level isolation. See SECURITY.md for details on the threat model and recommendations.
- Runner proxy — maintains long-poll sessions with GitHub's broker to receive job assignments
- Runner pool — 1-8 worker instances that execute jobs in sandboxed environments
- HTTP proxy — allowlist-based network isolation for runner traffic (GitHub, npm, PyPI, etc.)
- Build cache — persistent tool cache shared across job runs (Node.js, Python, etc.)
Add to your GitHub Actions workflow to automatically use localmost when available:
permissions:
actions: read
contents: read
jobs:
check:
uses: bfulton/localmost/.github/workflows/check.yaml@main
build:
needs: check
runs-on: ${{ needs.check.outputs.runner }}
steps:
- uses: actions/checkout@v4
# ... your stepsPrefer not to reference an external workflow? Copy the check inline:
jobs:
check:
runs-on: ubuntu-latest
outputs:
runner: ${{ steps.check.outputs.runner }}
steps:
- id: check
run: |
HEARTBEAT="${{ vars.LOCALMOST_HEARTBEAT }}"
if [ -n "$HEARTBEAT" ]; then
HEARTBEAT_TIME=$(date -d "$HEARTBEAT" +%s 2>/dev/null || echo "0")
AGE=$(($(date +%s) - HEARTBEAT_TIME))
if [ "$AGE" -lt 90 ]; then
echo "runner=self-hosted" >> $GITHUB_OUTPUT
exit 0
fi
fi
echo "runner=macos-latest" >> $GITHUB_OUTPUTThe check workflow uses a simple heartbeat mechanism:
- localmost automatically updates a
LOCALMOST_HEARTBEATvariable in your repo/org every 60 seconds - The workflow reads this variable and checks the timestamp
- If the timestamp is less than 90 seconds old → use
self-hosted - Otherwise → fall back to
macos-latest(or your configured fallback) - On clean exit, localmost immediately marks the heartbeat stale so workflows fall back without waiting
This fallback-to-cloud design is intentional: if your Mac is asleep, offline, or the heartbeat is stale for any reason, workflows continue running on GitHub-hosted runners rather than waiting or failing.
localmost uses a GitHub App for authentication. During installation, you'll be asked to grant the following permissions:
| Permission | Level | Purpose |
|---|---|---|
| Administration | Read & Write | Register and remove self-hosted runners on repositories |
| Actions | Read & Write | Check workflow status and cancel running jobs |
| Metadata | Read | Access basic repository information (required by GitHub for all apps) |
| Self-hosted runners (org) | Read & Write | Register and remove self-hosted runners at the organization level |
GitHub's permission model requires Administration: Read & Write for managing self-hosted runners at the repository level. This is the same permission scope needed by the official actions/runner registration process.
While this permission could theoretically allow other administrative actions, localmost only uses it for:
- Generating runner registration tokens (
POST /repos/{owner}/{repo}/actions/runners/registration-token) - Removing runners when you stop them (
DELETE /repos/{owner}/{repo}/actions/runners/{runner_id})
localmost is open source — you can verify this by searching for actions/runners in the codebase.
For organization-level runners, the narrower Self-hosted runners: Read & Write permission is used instead of Administration.
During GitHub App installation, you choose which repositories to grant access to:
- All repositories - localmost can register runners for any repo in your account/org
- Only select repositories - limit access to specific repos you want to run locally
You can change this at any time in your GitHub settings under Applications > Installed GitHub Apps > localmost > Configure.
localmost uses OAuth device flow authentication. Your access token is:
- Encrypted with macOS Keychain and stored locally
- Scoped only to the repositories you explicitly grant access to
- Revocable at any time from your GitHub settings
localmost includes a command-line interface for controlling the app from your terminal:
# Start/stop the app
localmost start
localmost stop
# Check runner status
localmost status
# Pause the runner (stops accepting new jobs)
localmost pause
# Resume the runner
localmost resume
# View recent job history
localmost jobsFrom the app menu: localmost → Install Command Line Tool...
This creates a symlink in /usr/local/bin so you can use localmost from any terminal. You'll be prompted for your administrator password.
For development builds, use npm link instead.
The CLI communicates with the running app via a Unix socket. Most commands require the app to be running - use localmost start to launch it first.
Built with Electron + React/TypeScript. Requires Node.js 18+.
# Clone and install dependencies
git clone https://github.com/bfulton/localmost.git
cd localmost
npm install
# Start the app in development mode
npm start
# Run tests
npm test
# Build for macOS (creates .dmg)
npm run makeCurrent release: 0.3.0 — Test Locally, Secure by Default
- Run workflows locally before pushing with
localmost test - Declarative sandbox policies with
.localmostrc - Secure secrets management in macOS Keychain
- Environment comparison with GitHub runners
Future feature ideas:
- Trusted contributors for public repos - Control which repos can run on your machine based on their contributor list. Options: never build public repos, only build repos where all contributors are trusted (default: you + known bots, customizable), or always build (with high-friction confirmation). Repos with untrusted contributors fail with a clear error.
- Quick actions - Re-run failed job, cancel all jobs.
- Spotlight integration - Check status or pause builds from Spotlight.
- Artifact inspector - Browse uploaded artifacts without leaving the app.
- Disk space monitoring - Warn or pause when disk is low, auto-clean old work dirs.
- Linux and Windows host support - Run self-hosted runners on non-Mac machines for projects that need them.
- Higher parallelism cap - Parallelize proxy registration to support 16+ concurrent runners (currently capped at 8 due to serial registration time).
- Ephemeral VM isolation - Run each job in a fresh lightweight VM for stronger isolation between jobs.
Bugs and quick improvements:
- Fix "build on unknown" race where jobs don't get links
