macOS companion app
macOS dev setup
macOS developer setup
Build and run the OpenClaw macOS application from source.
The packaged app requires macOS 15.0 or later. The build host must also meet the Xcode requirements below.
Prerequisites
- Xcode 26.4+ (Swift 6.3 toolchain), on the latest macOS available in Software Update.
- Node.js 24.16+ or 26.1+ & pnpm for the gateway, CLI, and packaging scripts.
macOS shell tooling uses the system /bin/bash (3.2); Homebrew Bash is not
required. Run scripts directly or with /bin/bash. Bash 5.3+ can stall on a
heredoc before its reader starts, leaving packaging or signing logs empty under
pipe-buffer pressure. Portable script entrypoints switch to /bin/bash on
macOS, and the streamed installers (curl ... | bash) do the same by capturing
the rest of their input into a private temporary file before re-executing, so
the documented install commands need no change.
1. Install dependencies
pnpm install2. Build and package the app
./scripts/package-mac-app.shOutputs 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 does not bypass the runtime build. Existing
content-checked build caches still avoid unnecessary declaration work.
Each worker keeps native binaries that support its architecture and omits incompatible macOS, Linux, and Windows prebuilds. This prevents unused Intel-only dependencies from triggering macOS compatibility warnings in Apple silicon builds. Compatible universal binaries, JavaScript, WASM, and other resources remain intact.
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).
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. Read the
version from the app: choose About OpenClaw in the menu bar, or run
openclaw-mac status --json, which reports the app version and build.
The npm command below is for npm 12 or npm 11.16+. On npm 11.15 and earlier,
omit --allow-scripts=openclaw.
npm install -g openclaw@<version> --allow-scripts=openclawpnpm 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:
node scripts/test-macos-native.mts named \ --package-path apps/macos --build-system native --enable-code-coverage \ --skip-build --filter AppStateIsolationTestsThe 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+).
xcodebuild -versionxcrun swift --versionIf 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:
OPENCLAW_SKIP_MLX_TTS=1 ./scripts/package-mac-app.shOtherwise, 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.
-
Reset TCC permissions for the debug bundle id:
bash tccutil reset All ai.openclaw.mac.debug -
If that fails, temporarily change
BUNDLE_IDinscripts/package-mac-app.shto force a clean slate from macOS.
Gateway "Starting..." indefinitely
Check whether a zombie process holds the port:
openclaw gateway statusopenclaw gateway stop # If you're not using a LaunchAgent (dev mode / manual runs), find the listener:lsof -nP -iTCP:18789 -sTCP:LISTENIf a manual run holds the port, stop it (Ctrl+C), or kill the PID found above as a last resort.