Guide

How to run a CircleCI self-hosted runner on a Mac

CircleCI machine runner 3 installs on macOS from CircleCI's Homebrew tap and runs as a LaunchAgent. Create a namespace and resource class, copy the token, put it in the runner's config.yaml, and bootstrap the service. Jobs reach it with machine: true and resource_class: namespace/name.

What you need first

  • Organization admin rights in CircleCI. An admin must accept the runner terms under Org, then Runners, before the menu appears.
  • At least one credit on the account. CircleCI says runner jobs do not use credits, but storage and network transfer can.
  • An Apple silicon Mac with admin access, Homebrew and Xcode.
  • sha256sum, which CircleCI lists as a prerequisite. Get it with brew install coreutils.

1. Create a namespace and resource class

In the web app, open Runners and select Create Resource Class. Each organization gets one namespace. If you publish orbs, you already have it. Name the resource class something like mac-mini-m6. Save, and copy the token. CircleCI shows it only once.

The CLI does the same thing:

circleci namespace create <name> --org-id <your-organization-id>
circleci runner resource-class create <namespace>/<resource-class> <description> --generate-token

2. Install the runner with Homebrew

brew tap circleci-public/circleci
brew trust circleci-public/circleci
brew install circleci-runner

The middle line is new. Homebrew 7.0.7, the version on our Mac in October 2026, refuses to load packages from a third-party tap until you trust it. CircleCI's page does not show this step yet. The runner arrives as a Homebrew cask.

macOS may show a notice that a background item from Circle Internet Services was added. That is expected. Homebrew also writes the LaunchAgent plist to ~/Library/LaunchAgents/com.circleci.runner.plist.

3. Add the token to config.yaml

nano $HOME/Library/Preferences/com.circleci.runner/config.yaml
runner:
  name: "mac-mini-m6"
  working_directory: "/Users/$USER/Library/com.circleci.runner/workdir"
  cleanup_working_directory: true
api:
  auth_token: "your-resource-class-token"

With cleanup_working_directory on, each job starts from a clean checkout. Xcode's DerivedData lives in ~/Library/Developer/Xcode/DerivedData by default, so build caches still survive. If you pass -derivedDataPath inside the working directory, cleanup deletes it after every job.

Protect the token

The resource class token is what lets a machine claim jobs for that class. Anyone who reads it can attach their own machine and receive your jobs, with your secrets. Lock the config file to your user, and rotate the token if it ever leaks.

chmod 600 $HOME/Library/Preferences/com.circleci.runner/config.yaml
ls -l $HOME/Library/Preferences/com.circleci.runner/config.yaml

The same logic applies to jobs. They run as the macOS user that started the runner, on the same disk as everything else. Point only trusted projects at this resource class.

4. Accept the notarization

The binary comes from the internet, so macOS must approve it. CircleCI documents checking the signature first, then removing the quarantine flag.

spctl -a -vvv -t install "$(brew --prefix)/bin/circleci-runner"
sudo xattr -r -d com.apple.quarantine "$(brew --prefix)/bin/circleci-runner"

The first command should say accepted, with source Notarized Developer ID.

5. Start the runner in the GUI domain

Run these from Terminal on the Mac's desktop. The GUI domain is the logged-in desktop session. That is where the Simulator and the login keychain live.

launchctl bootstrap gui/$(id -u) $HOME/Library/LaunchAgents/com.circleci.runner.plist
launchctl enable gui/$(id -u)/com.circleci.runner
launchctl kickstart -k gui/$(id -u)/com.circleci.runner
launchctl print gui/$(id -u)/com.circleci.runner

CircleCI also documents a user domain option for headless sessions. It moves the plist to /Library/LaunchAgents. On macOS 27 we saw a plist in that folder break auto-login on every boot. See auto-login on a headless Mac. For iOS work, we suggest the GUI domain with automatic login.

6. Make it survive a reboot

A plist in ~/Library/LaunchAgents loads when its user logs in. Turn on automatic login for this account in System Settings, under Users and Groups. Turn off sleep with sudo pmset -a sleep 0. Reboot once and check launchctl print again. Logs are in ~/Library/Logs/com.circleci.runner/runner.log.

7. Point a job at the Mac

version: 2.1

jobs:
  ios-tests:
    machine: true
    resource_class: your-namespace/mac-mini-m6
    steps:
      - checkout
      - run:
          name: Run tests
          command: |
            rm -rf build/TestResults.xcresult
            xcodebuild test -project MyApp.xcodeproj -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.5' -resultBundlePath build/TestResults.xcresult
      - run:
          name: Zip result bundle
          when: always
          command: ditto -c -k --keepParent build/TestResults.xcresult TestResults.xcresult.zip
      - store_artifacts:
          path: TestResults.xcresult.zip

workflows:
  ios:
    jobs:
      - ios-tests

CircleCI's docs name two fields a runner job must have: machine: true and the resource_class. There is no macos: xcode: key here, because you do not pick an image. The job uses whatever Xcode the Mac has selected. On a MacRun Mac that is Xcode 26.6 with the iOS 26.5 simulator runtime.

Common errors and fixes

  • Refusing to load cask ... from untrusted tap. Run brew trust circleci-public/circleci, then install again.
  • macOS blocks the binary. You skipped the notarization step. Run the xattr command above.
  • Jobs queue but never start. Check that the resource class in config.yml matches the one you created, then read runner.log.
  • Docker layer caching does not work. CircleCI lists it as unsupported on self-hosted runners.
  • xcodebuild: error: Existing file at -resultBundlePath. Delete the old bundle before the test step. We hit this on Xcode 26.6.

To stop the runner, use launchctl bootout gui/$(id -u)/com.circleci.runner. To remove it, run brew uninstall --cask circleci-public/homebrew-circleci/circleci-runner.

Why a dedicated Mac helps

Warm caches are the main win. In our benchmark we built the Wikipedia iOS app with Xcode 26.6. A typical job after a small change took 27 seconds on a warm M6. It took 269 seconds on a fresh GitHub-hosted macos-26 runner. A clean build took 86 seconds against 183.

When CircleCI's hosted macOS is enough

CircleCI runs its own Mac executors. Its docs, read in October 2026, list m4pro.medium with 6 vCPUs and 28 GB, and m4pro.large with 12 vCPUs and 56 GB. Both have more memory than our 16 GB M6. If your suite needs that much memory, or you build a few times a week, stay hosted. Skip MacRun too if you need an SLA, a static IP, or several regions.

See other CI systems for the MacRun side of the setup, or compare costs with your own minutes.

Frequently asked questions

Is the CircleCI self-hosted runner free?

+

CircleCI says runner execution does not use credits. You need at least one credit on the account, because storage and network transfer can still be billed.

Where is the CircleCI runner config on macOS?

+

In $HOME/Library/Preferences/com.circleci.runner/config.yaml. Logs go to $HOME/Library/Logs/com.circleci.runner/runner.log.

Should I use the GUI domain or the user domain?

+

For iOS builds, the GUI domain with automatic login. It runs inside the desktop session that the Simulator and the login keychain need.

Does Docker layer caching work on a CircleCI self-hosted runner?

+

No. CircleCI lists Docker layer caching as not supported on self-hosted runners.

Related guides