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 | vs GitHub |
|---|---|---|
| GitHub macos-latest | 967s | — |
| MacBook Air M2 (2022) | 202s | 4.8x faster |
| MacBook Pro M4 Max (2024) | 77s | 12.6x faster |
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-16 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.
- Runner proxy — maintains long-poll sessions with GitHub's broker to receive job assignments
- Runner pool — 1-16 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)
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 jobsAfter installing localmost.app, create a symlink to add the CLI to your PATH:
sudo ln -sf "/Applications/localmost.app/Contents/Resources/localmost-cli" /usr/local/bin/localmostOr for development builds:
npm linkThe 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 makeFuture feature ideas:
- Quick actions - Re-run failed job, cancel all jobs.
- Audit logging - Detailed logs of what each job accessed.
- Network policy customization - User-defined network allowlists per repo.
- Workflow testing mode - Run and validate workflows locally before pushing.
- 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.
- Runner handoff - Transfer a running job to GitHub-hosted if you need to leave.
- Reactive state management - Unify disk state, React state, and state machine into a single reactive store to prevent synchronization bugs.
Bugs and quick improvements:
- Fix the CLI install process to be polished
- Fix "build on unknown" race where jobs don't get links
