MacOn › Documentation

MacOn Documentation

Everything for MacOn: the iPhone & iPad companion, the Mac app, and the macon CLI.

Prefer the raw docs? The same reference lives on GitHub. View on GitHub ↗

Overview

MacOn puts your Mac in your pocket. Pair an iPhone or iPad and you can see and drive its screen, wake and unlock it even when it is asleep or locked, automate it with a deck and flows, code on it, and hand tasks to an AI that operates it, from anywhere. The same Mac also runs as a local iOS CI runner: it watches a Bitbucket or GitHub repository, runs the pipeline in your macon.yml, and ships to TestFlight, all on hardware you own.

It comes in three parts that share one engine:

Anything you set up in the app can be exported and run by the CLI, and vice-versa. The Mac app and companion share one soft 3D "clay world" look you can re-skin across seventeen themes.

Install

Companion (iPhone & iPad)

Join the free beta on TestFlight: testflight.apple.com/join/PgHKv8WC. Requires iOS or iPadOS 17 or later. On first launch, pair it with your Mac using a one-time code (see Pairing & devices).

Mac app

Download the latest build here. Requires macOS 14 or later. The app hosts the companion server the phone connects to, and runs your CI.

CLI (Homebrew)

Optional, for headless and scripted use, the same engine as the app:

brew tap alimusawa313/macon https://github.com/alimusawa313/homebrew-macon brew install macon

First run: macon init / macon doctor

Before watching anything, confirm the machine has what an iOS build needs. macon init checks everything and installs the Homebrew-managed pieces automatically; macon doctor is the report-only health check.

macon init # check + install what's missing macon doctor # report only, install nothing macon doctor --json # machine-readable; non-zero exit when something's missing

It verifies Homebrew, Xcode + Command Line Tools, git, Ruby/Bundler, fastlane, SwiftLint, gitleaks, a JDK, cloudflared, simulator runtimes, disk space (warns under 20 GB free, DerivedData eats it), and the TCC permissions (Screen Recording, Accessibility) the current process would need to serve screen/control headless. Tools available via Homebrew are installed for you; Xcode and simulators print the exact command to run.

CLI overview

CommandWhat it does
macon doctor | init [--check]Check toolchain, permissions, cloudflared, disk; init installs what's missing.
macon sims …List / install simulator runtimes & devices.
macon lint [path]Validate and summarize a macon.yml.
macon run …Run one workflow once, then exit.
macon watch …Watch a repo and build new commits (until Ctrl-C).
macon status | logs | trigger | cancelRemote-control a running runner from the terminal.
macon pair --host H --code CPair this CLI with a remote runner (local pairs itself).
macon metricsPrint the runner's Prometheus metrics.
macon config <init|validate>Scaffold / check the portable config.
macon validate [path]Validate a macon.yml or an export file (by extension).
macon service … / install-serviceRun a watch as a background launchd service.
macon companion …Manage companion-app (iPhone/iPad) pairings.
macon pipelines [file]List pipelines in an app export file.
macon completions <zsh|bash>Print shell completions.

Simulators

Inspect and manage the simulators your test matrix targets, for iOS, watchOS, tvOS, and visionOS (macOS builds run natively, no simulator).

macon sims # list runtimes + device types macon sims install 18.1 # iOS runtime (bare version ⇒ iOS) macon sims install watchOS 11.2 # any platform + version macon sims create "iPhone 16" 18.1 # create a specific device

Use macon sims to find the exact device and os values for a test matrix.

Run once

Run a single workflow in a checkout and exit with its status code, perfect for a git hook, cron, or another CI.

macon run --workflow test # in the current directory macon run --workflow beta ~/src/planpal # in a given repo # secrets come from the shell environment ASC_KEY_ID=… ASC_ISSUER_ID=… ASC_KEY_CONTENT=… macon run --workflow beta

Watch

Continuously build new commits (or pull requests). Runs until you stop it.

# poll a branch on Bitbucket macon watch --workspace acme --repo app --branch main # open PRs on GitHub, instant via webhook macon watch --provider github --workspace org --repo app \ --prs --webhook --port 8787 --webhook-secret "$SECRET" # everything you set up in the app macon watch --config macon-export.json

Options

FlagMeaning
--provider bitbucket|githubGit host (default bitbucket). GitHub: workspace = owner/org.
--branch B / --prsWatch a branch, or open PRs (--pr-target to filter).
--webhook [--port N]Push mode, build the instant the host calls you.
--webhook-secret SRequire a secret (GitHub HMAC, or in the URL path).
--every SECSPoll interval for polling mode (default 30).
--timeout MINSCancel a build that runs longer than this.
--workflow N / --fileWhich workflow / which pipeline file.
--no-statusDon't post build status back to the host.
--companion [--companion-port N]Serve the iPhone/iPad app (default port 8899).

Remote control, status · logs · trigger · cancel

Drive a running runner from the terminal, the desktop app's companion server, or a headless macon watch --companion, local or across the network. Full control without the GUI.

macon status # pipelines + builds (add --json for scripts) macon logs # log of the live (or newest) build macon logs "My App" --follow # tail until it finishes; exit 0/1 by result macon trigger "My App" --follow # build the current head now, wait for the verdict macon cancel # stop whatever is running

Auth is the same device token a paired iPhone uses. Against a local runner there's nothing to do, the CLI pairs itself through the shared on-disk pairing store and keeps the token in your Keychain. Against a remote runner (tunnel, EC2 Mac), pair once with the code the runner prints:

macon pair --host name.trycloudflare.com --code K7QP-2M9X-4RTD macon status --host name.trycloudflare.com

Pipelines match by name, id, or unique prefix. --follow exits 0/1 by the build's result, so macon trigger app --follow drops straight into another CI as a gate. --json (on status, doctor, config validate) makes everything scriptable.

Prometheus metrics

Every runner serves GET /metrics, Prometheus text exposition, behind the same bearer token: macon_up, pipeline/watching/building gauges, per-pipeline build totals by result, and last-build duration. macon metrics prints it; point a scraper at it for dashboards:

# prometheus.yml scrape_configs: - job_name: macon static_configs: [{ targets: ["mac-mini.local:8899"] }] authorization: { credentials: "<device token>" }

Portable config & validate

Scaffold and sanity-check the macon-export.json the CLI runs from, without opening the app.

macon config init # write a starter macon-export.json macon config validate [path] # field checks; exit 1 on errors (--json) macon validate <path> # .json → export checks, else lints a macon.yml

validate catches empty workspace/repo/branch, duplicate webhook ports, missing provider credentials, and secrets that are in neither the file nor the environment, warnings for the soft stuff, errors for what would break a watch.

Run as a service

Install a watch as a launchd agent so it starts at login and restarts on crash, the way to run MacOn unattended on a dedicated or cloud (EC2) Mac. macon install-service is the one-line alias.

macon install-service --config ~/macon-export.json --companion macon service install --provider github --workspace org --repo app --webhook --label ci macon service status [--label NAME] macon service uninstall [--label NAME]

Credentials present in your shell at install time (BITBUCKET_*, GITHUB_TOKEN, ASC_*, SLACK_URL) are copied into the agent. Logs go to ~/Library/Logs/macon/<label>.log.

macon.yml, structure

A pipeline lives in your repo as macon.yml. It's Bitrise-style: named workflows composed with before_run/after_run, triggers that route branches/PRs to workflows, environment variables, and conditional or always-run steps.

name: PlanPal iOS CI workflows: _setup: steps: - { name: Gems, script: bundle install } - { name: SwiftLint, script: swiftlint lint --strict } test: before_run: [_setup] steps: - name: UI Tests script: bundle exec fastlane test beta: before_run: [test] steps: - { name: TestFlight, script: bundle exec fastlane beta } triggers: - { pull_request: "*", workflow: test } - { branch: main, workflow: beta }

Workflows

Each workflow is a named list of steps. Compose them so shared setup runs once:

Pick which workflow runs with --workflow, or leave it to the triggers.

Steps

A step is a name and a shell script. Each becomes its own timed section in the app's log.

FieldMeaning
nameShown in logs.
scriptShell to run (zsh, in the checkout).
run_ifOnly run if this condition exits 0. e.g. '[ -n "$MACON_PR_ID" ]'
always_runtrue to run even if an earlier step failed.
matrixFan the step out over combinations, see below.

Test matrix

Fan a step out across every combination of values, ideal for UI-testing on multiple devices and OS versions. Each run gets MACON_MATRIX_<KEY> in its environment. All combinations run; the step fails if any of them fails.

- name: UI Tests matrix: device: ["iPhone 17 Pro", "iPad Air 11-inch (M4)"] os: ["26.4", "26.5"] # → 4 runs script: bundle exec fastlane test device:"$MACON_MATRIX_DEVICE" os:"$MACON_MATRIX_OS"

Combinations run sequentially so simulators don't contend. Match the values to installed runtimes (macon sims).

Triggers

Route branches and pull requests to workflows (like Bitrise's trigger map). When a pipeline's workflow field is blank, MacOn matches here.

triggers: - { pull_request: "*", workflow: test } # any PR - { branch: main, workflow: beta } - { branch: "dev-*", workflow: test } # globs supported

Built-in variables

Every step runs with these set:

VariableValue
MACON_COMMIT, MACON_COMMIT_SHORTThe commit being built.
MACON_BRANCHBranch (or PR source branch).
MACON_REPOworkspace/repo.
MACON_WORKFLOWThe running workflow's name.
MACON_BUILD_NUMBERIncrementing build count.
MACON_PR_ID, MACON_PR_TITLE, …Set on pull-request builds.
MACON_MATRIX_*Set inside a matrix step.

Provider-native variables (BITBUCKET_* or GITHUB_*) are also injected so tools like Danger work.

The second mode: a Bitbucket runner pool

Prefer to keep your existing bitbucket-pipelines.yml and let Bitbucket stay the brain? MacOn's other CI mode hosts a pool of Bitbucket self-hosted runners on your Mac. Bitbucket parses the YAML, schedules jobs, shows logs and gates PRs, your Mac just does the building.

Both modes run happily at once: macon.yml pipelines for your own repos, a runner pool for the team's Bitbucket workspace.

Bitbucket & GitHub

A pipeline watches one host. Set credentials once (in the app's Settings, or via environment for the CLI).

ProviderAuthEnvironment
BitbucketEmail + API tokenBITBUCKET_EMAIL, BITBUCKET_API_TOKEN
GitHubPersonal Access TokenGITHUB_TOKEN

The GitHub token needs repo access, classic repo scope, or fine-grained with Contents + Commit statuses + Pull requests.

Secrets

Secrets your pipeline needs (ASC_KEY_ID, ASC_ISSUER_ID, ASC_KEY_CONTENT, SLACK_URL, …) are injected into every step. In the app they're stored in the macOS Keychain, never in the repo. For the CLI they come from the inherited shell environment, unless you exported the config with secrets.

Shipping to TestFlight

MacOn runs your fastlane lanes, so shipping is a normal beta lane with automatic signing and an App Store Connect API key.

  1. Create an API key in App Store Connect → Users and Access → Integrations. Download the .p8 once.
  2. Provide it as three secrets: ASC_KEY_ID, ASC_ISSUER_ID, and ASC_KEY_CONTENT (the .p8, base64-encoded).
  3. Route main to a workflow that runs fastlane beta.
# base64 of the .p8, straight to your clipboard base64 -i ~/Downloads/AuthKey_XXXX.p8 | pbcopy

App ↔ CLI export

Set everything up visually in the app, then run it headless. In the app: Settings → Export Configuration… writes a macon-export.json (optionally including secrets). Then:

macon pipelines macon-export.json # see what's inside macon watch --config macon-export.json # run them all macon service install --config macon-export.json # …as a service

Exported without secrets, the file is config-only and safe to commit; supply token/secret values via the environment. Exported with secrets, it's self-contained, keep it private. The app's Import… loads a file back, so it also moves a setup between machines.

Companion app (iPhone & iPad)

The MacOn companion app turns your phone or iPad into a remote for the runner: a home hub with your pipelines, live build logs, screen streaming, remote control, a Stream Deck-style shortcuts board, a flows canvas, a code workspace, and AI, handy when the runner is a Mac in another room or a headless cloud instance. Add --companion to any watch to serve it:

macon watch --workspace acme --repo app --branch main --companion macon watch --config macon-export.json --companion # with your app setup

Builds show as a structured step timeline, each step with its own status, duration, and expandable output (parsed the same way the desktop app does it), with the raw log one tap away. Re-run and Cancel sit right in the build view and on a long-press of any run.

Prefer the desktop app? Flip it on in Settings → Companion app, then Pair a device shows the address, code, and a QR to scan. Pairings are shared with the CLI, so a device works with either.

The app is a separate open-source project, pair it once and it reconnects on its own. Get the companion (TestFlight) ↗

Pairing & devices

On start, the companion server prints a one-time address and code. In the app, tap Add runner and enter them (or scan the QR), the code is exchanged for a long-lived device token stored in the iOS Keychain, so you only pair once. Codes are single-use, expire (15 min by default), and are rate-limited.

┌─ Pair the MacOn companion app ────────────────────── │ Address: alis-mac.local:8899 │ Code: K7QP-2M9X-4RTD · valid 15 min, one device └──────────────────────────────────────────────────────

Headless or cloud (EC2) Macs have no screen to read, the code prints to the log, so grab it over SSH. Expose the port through a cloudflared tunnel and point the app at the tunnel host (the app talks HTTPS/WSS):

cloudflared tunnel --url http://localhost:8899 # → https://<name>.trycloudflare.com

Manage paired devices from the CLI, file-based, so it works even with no server running:

macon companion devices # list paired iPhones/iPads macon companion revoke <prefix> # revoke one (token prefix from `devices`) macon companion revoke-all # revoke everything

Remote access, from anywhere

By default the companion talks to the Mac over your local network. To reach it from cellular or another network, the desktop app can open a free internet tunnel for you: Settings → Companion app → Remote access. It runs cloudflared (a one-time brew install cloudflared) and shows a public https://…trycloudflare.com address, paste that into the app instead of the LAN address.

Manage pipelines from the device

The Pipelines screen isn't just a viewer. Each pipeline card has a watch toggle (start/stop the runner's watcher) and a long-press menu with Run Now, Edit, and Delete. The + button adds a new pipeline with the full editor, name, provider, workspace, repository and branch picked from searchable dropdowns (listed through the Mac's saved credentials; tokens never leave the Mac), watch mode, polling/webhook trigger, pipeline file, and fallback build command.

Screen streaming

Beyond builds and logs, a paired device can watch the Mac's screen live, handy for eyeballing a simulator during UI tests, or driving a Mac in another room. Tap the display icon on the dashboard. The Mac captures with ScreenCaptureKit and hardware-encodes H.264 (VideoToolbox); the iPhone/iPad decodes in hardware and displays it. Capture runs only while someone is watching.

Remote control

While viewing the screen, turn on Control the Mac from the Touch Bar to drive it from the iPad. Two ways to move the cursor:

A Sidecar-style Touch Bar sits under the screen; swipe it left/right between pages. Page one: esc, switch spaces (◂ ▸), Mission Control, right-click, trackpad, keyboard, plus the resolution and fps pickers. Page two: the sticky ⌘ ⌥ ⌃ ⇧ modifier keys plus Undo, Spotlight, App switch, and Dock. Page three: media keys and a volume slider. Tap a modifier to arm it, then tap or type, e.g. then C sends ⌘C, or then a tap sends a ⌘-click. A hardware keyboard passes straight through.

CompactOS, Mac apps, phone-sized

CompactOS makes a Mac app usable from your phone as if it were a native one. Open the CompactOS tab and you get a grid of the Mac's installed apps and open windows. Tap one and the Mac launches or focuses it, resizes that window to your device's screen (a fullscreen app becomes windowed first), and streams just that window, so the text is big and readable instead of a shrunk-down whole desktop.

It needs “Let paired devices control this Mac” enabled on the Mac (Accessibility permission).

Shortcuts deck (Stream Deck)

The Shortcuts board turns the iPad into a Stream Deck for the Mac: a grid of Liquid Glass keys that launch Mac apps (with their real icons, fetched from the Mac), fire keyboard shortcuts (⌘⌥⌃⇧ + key), or run a sequence, a chain of steps like open Chrome → wait → type a URL → Return.

Flows, visual automations

Flows are block-graph automations you draw on the device and run on the Mac. Blocks snap onto a pannable, zoomable canvas and wire together with ropes that physically sag and swing (a verlet simulation); data flows along the wires from block to block.

Code workspace, editor, terminals, Xcode

The workspace is a mini IDE pointed at a folder on your Mac: a collapsible file explorer, a tabbed editor (type locally with zero latency; Save syncs the file back), and a side-by-side code assistant that reads the open file as context, @-mention other files to fold them in. Assistant models: Claude with your key, or local Ollama.

AI features, chat, voice, and the agent

Every AI feature is optional and bring-your-own: Anthropic Claude, OpenAI, Google Gemini, any OpenAI-compatible endpoint, or fully local models via Ollama running on the Mac. Keys are stored in the Mac's Keychain and never sync to the phone.

Fleet map

The Fleet screen draws your setup as a little world: the Mac at the center with every paired device hanging off it on swinging, draggable strings (the same verlet ropes as the flows canvas). Devices that talked to the Mac recently glow; quiet ones fade. Tap a card for its pairing details, device model, last-seen time, token prefix, and revoke from there or with macon companion devices. The Mac app has the same map, so you can audit pairings from either end.

Wake & unlock the Mac

The companion can keep the Mac reachable and bring it back to life remotely. Everything here is off by default and toggled on the Mac in Settings → Power & Access.

No special entitlement is needed, Wake-on-LAN is a plain UDP broadcast, and the Mac app isn't sandboxed. On the phone, “Stay connected” keeps reconnecting (and fires a wake packet) whenever the Mac drops off.

Always connected, via iCloud

Optional iCloud (CloudKit) sync makes the pairing self-healing. Turn it on in both apps (same Apple ID) and the Mac publishes its current address, including a freshly rotated Cloudflare tunnel URL, to your private iCloud database. The phone reads that beacon and re-points itself automatically, reconciling against the Mac's real address on every poll. Pair once and you never re-type an address again, no matter how the tunnel URL changes.

Privacy screen

When you're driving the Mac remotely, the privacy screen covers its physical display with a “please don't touch” wall, so a passerby can't see what you're doing, while the companion keeps viewing and controlling the real desktop underneath. Raise it from Settings → Companion app or the menu-bar extra; dismiss it with the ⌃⌥⌘U hot key.

Themes, the clay world

Both the Mac app and the companion are dressed as a soft, tactile 3D clay world (rendered with SceneKit), and it's re-skinnable. Pick a world in Settings and the whole app repaints, backdrop, cards, and the floating 3D models on the home.

That's the full picture. For the exhaustive flag-by-flag reference, see the CLI.md on GitHub ↗.