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
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 canonicalscripts/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 temporaryHOME, 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:
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 atFreezing 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+).Build fails: MLX voice helper Metal shaders
On a beta-only Xcode toolchain (for example Xcode 27 with the macOS 27 SDK), only theopenclaw-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:
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:
-
If that fails, temporarily change
BUNDLE_IDinscripts/package-mac-app.shto force a clean slate from macOS.