Skip to main content

macOS developer setup

Build and run the OpenClaw macOS application from source.

Prerequisites

  • Xcode 26.4+ (Swift 6.3 toolchain), on the latest macOS available in Software Update.
  • Node.js 24.15+ & pnpm for the gateway, CLI, and packaging scripts. Node 22.22.3+ also works.

1. Install dependencies

2. Build and package the app

Outputs dist/OpenClaw.app. Packaging requires a real signing identity by default and fails if none is available. Ad-hoc signing is an explicit opt-in; it does not preserve TCC permissions. See macOS signing. Packaging builds the JavaScript runtime and Control UI, then provisions a private Node worker from the canonical package artifact for every requested BUILD_ARCHS architecture. The root worker tarball uses the repository-pinned pnpm packer; Corepack-only setups are supported. Packaging verifies native capabilities and worker readiness in temporary state before and after signing, then replaces the previous app. scripts/restart-mac.sh uses the same path; SKIP_TSC=1 no longer bypasses the runtime build. Existing content-checked build caches still avoid unnecessary declaration work. Universal builds require both arm64 and x86_64 runtimes to execute during validation. Building x86_64 on Apple Silicon requires Rosetta; a missing architecture or nonportable native dependency fails packaging. Node downloads and package installation need network access. The larger app includes its complete private runtime; it does not update an independently managed Gateway. Packaging builds the MLX voice helper with Swift Build (--build-system swiftbuild) and copies its SwiftPM resource bundles into Contents/Resources. The native SwiftPM backend does not compile MLX’s Metal shaders. Packaging fails if the helper’s mlx-swift_Cmlx.bundle/Contents/Resources/default.metallib is missing, rather than shipping a helper that fails on its first speech request. The private worker uses read-only core config bootstrap rather than Gateway-wide Doctor preflight. Node plugin validation, MCP lifecycle, and node-owned identity and exec-approval startup migrations remain enabled. See Gateway ownership for the boundary. Set OPENCLAW_SKIP_MLX_TTS=1 to package a dev/proof build without the local MLX voice helper. This skips the openclaw-mlx-tts binary and its large mlx-swift Metal shader stack, which some beta Xcode toolchains cannot compile. The resulting app has no on-device MLX voice; it is rejected for release builds, which must ship the helper. For dev run modes, signing flags, and Team ID troubleshooting, see apps/macos/README.md. Fast dev loop from repo root: scripts/restart-mac.sh (add --no-sign for ad-hoc signing; TCC permissions do not stick with --no-sign).
Ad-hoc signed apps may trigger security prompts. If the app crashes immediately with “Abort trap 6”, see Troubleshooting.

3. Install the CLI and Gateway

The packaged app embeds the canonical scripts/install-cli.sh installer. On a fresh profile, choose This Mac during onboarding; the app installs the matching user-space CLI and runtime before starting the Gateway wizard. For manual development recovery, install the matching CLI yourself: The npm command below is for npm 12 or npm 11.16+. On npm 11.15 and earlier, omit --allow-scripts=openclaw.
pnpm add -g --allow-build=openclaw openclaw@<version> and bun add -g --trust openclaw@<version> also work. Bun’s --trust allows the OpenClaw lifecycle scripts for that install. Node remains the recommended runtime for the Gateway itself.

Run native tests safely

Run the full macOS app test suite in a disposable macOS VM or CI worker with no operator credentials, config, or running Gateway. AppKit tests can show windows, and WebKit starts helper processes. A temporary HOME, TMPDIR, or named app profile alone is not a sandbox: fixed preferences domains and Keychain access can still reach macOS services outside those directories. The macos-swift GitHub CI job builds the tests with the runner’s normal SwiftPM caches, then runs the built suite through scripts/test-macos-native.mts. Each invocation selects private HOME and CFFIXED_USER_HOME, OPENCLAW_CONFIG_PATH, OPENCLAW_STATE_DIR, and short TMPDIR before any test bundle loads. Tools honoring TMPDIR use that launcher-owned directory; Foundation uses Darwin’s per-user temp directory, owned and discarded by the disposable OS worker. The full suite explicitly selects the default profile, preserving its local Gateway lifecycle contracts. AppState isolation tests run separately with a unique named profile; no test is run twice. The child environment excludes inherited app settings and credentials while retaining toolchain and runtime loader paths. Before Swift starts, the launcher creates an empty-password test Keychain under its private HOME/Library/Keychains, unlocks it, disables automatic locking for that resource, and sets the user-domain default and search list to that file. This lets real catalog migration save without an interactive login-Keychain creation prompt. Its Security preferences live in the private HOME/Library/Preferences; common and dynamic Keychain domains remain unchanged. Resources remain available for process-lifetime singletons and retained windows; the launcher deletes its Keychain and files after the managed process group and output pipes close. The disposable runner owns default preferences and system-service state; named-profile preferences use a fresh domain and are also discarded with the runner. If process cleanup cannot be verified or Keychain deletion fails, the launcher fails and retains its files for inspection. CI environment markers only catch accidental invocation; they do not enforce isolation or make an operator desktop safe. For a focused rerun inside that disposable macOS CI runner, after the normal test build:
The ordinary CI invocation bounds Swift Testing parallelism to the runner’s logical CPU count, capped at 12, and runs the default and named partitions sequentially with coverage. Local scripts/prepush-ci.sh runs Swift lint/format checks and a release build, but does not run native tests. For native changes it exits nonzero with a requirement to obtain the exact commit’s macos-swift CI result; local build success is not native test success. On an operator desktop, run only an audited subset inside an OS sandbox that blocks operator files, preferences and Keychain services, unwanted network access, and desktop/helper processes. A Swift test filter is not itself an isolation boundary. If that boundary cannot be established, use the disposable macOS environment instead. Tests should own their resources: unique defaults suites with cleanup, nonpersistent WebKit data stores, ephemeral loopback fixture endpoints, and temporary files rooted in FileManager.temporaryDirectory. Unix-domain socket fixtures require a short test-owned path there; an overlong path fails rather than silently writing outside that directory. The cooperative TestIsolation helper serializes and restores participating tests’ environment and selected defaults mutations. Config-only scopes also own a temporary state directory for config health/audit writes and remove it when the async body finishes, including errors. Callers still own their config fixture files and must join any async work before leaving the scope. These unique fixture directories are cleaned by their owners; remaining Foundation temporary files are discarded with the worker, not the launcher’s root. Never change OPENCLAW_PROFILE inside a test: AppProfile and AppDefaults freeze their identity for the process. Tests needing another singleton identity require a fresh process. The cooperative helper does not isolate unrelated tests or the process from the host.

Troubleshooting

Build fails while freezing Peekaboo sources

If packaging stops at Freezing authenticated Peekaboo sources in a read-only snapshot, check the hdiutil error on stderr. Routine image creation and attachment output stays quiet, but failures such as hdiutil: attach failed - Permission denied are preserved. Snapshot images and build outputs stay in the checkout. Their read-only mount directories use macOS’s per-user temporary location (getconf DARWIN_USER_TEMP_DIR), independently of TMPDIR, so an external checkout does not need to support nested mounts. If attachment still fails, check that location’s mount permissions. Unverified cleanup retains the mount directories and build locks at the paths printed in the error. This step runs before signing; source verification and the read-only snapshot remain required.

Build fails: toolchain or SDK mismatch

The macOS app build expects the latest macOS SDK and the Swift 6.3 toolchain (Xcode 26.4+).
If versions don’t match, update macOS/Xcode and re-run the build.

Build fails: MLX voice helper Metal shaders

On a beta-only Xcode toolchain (for example Xcode 27 with the macOS 27 SDK), only the openclaw-mlx-tts helper may fail while the main app builds fine. The mlx-swift Metal compilation errors non-deterministically (a different .metal file each run, Could not read serialized diagnostics file then a nonzero metal exit), because the beta metal compiler and its separately downloaded Metal Toolchain are still unstable. This is an upstream toolchain issue, not an OpenClaw one. If you do not need on-device MLX voice, skip the helper:
Otherwise, install the Metal Toolchain (xcodebuild -downloadComponent MetalToolchain) and build from a stable Xcode release.

App crashes on permission grant

If the app crashes when you try to allow Speech Recognition or Microphone access, it may be a corrupted TCC cache or signature mismatch.
  1. Reset TCC permissions for the debug bundle id:
  2. If that fails, temporarily change BUNDLE_ID in scripts/package-mac-app.sh to force a clean slate from macOS.

Gateway “Starting…” indefinitely

Check whether a zombie process holds the port:
If a manual run holds the port, stop it (Ctrl+C), or kill the PID found above as a last resort.