docs/platforms/macos.md
The macOS app is the menu-bar companion for OpenClaw. It owns permissions, manages/attaches to the Gateway locally (launchd or manual), and exposes macOS capabilities to the agent as a node.
system.run).openclaw) on request via npm, pnpm, or bun (the app prefers npm, then pnpm, then bun; Node remains the recommended Gateway runtime).openclaw gateway install.The app manages a per-user LaunchAgent labeled ai.openclaw.gateway
(or ai.openclaw.<profile> when using --profile/OPENCLAW_PROFILE; legacy com.openclaw.* still unloads).
launchctl kickstart -k gui/$UID/ai.openclaw.gateway
launchctl bootout gui/$UID/ai.openclaw.gateway
Replace the label with ai.openclaw.<profile> when running a named profile.
If the LaunchAgent isn't installed, enable it from the app or run
openclaw gateway install.
If the gateway repeatedly disappears for minutes to hours and only resumes when you touch the Control UI or SSH into the host, see the troubleshooting note for macOS Maintenance Sleep / ENETDOWN crashes and launchd's respawn-protection gate in Gateway troubleshooting.
The macOS app presents itself as a node. Common commands:
canvas.present, canvas.navigate, canvas.eval, canvas.snapshot, canvas.a2ui.*camera.snap, camera.clipscreen.snapshot, screen.recordsystem.run, system.notifyThe node reports a permissions map so agents can decide what's allowed.
Node service + app IPC:
system.run executes in the macOS app (UI/TCC context) over a local Unix socket; prompts + output stay in-app.Diagram (SCI):
Gateway -> Node Service (WS)
| IPC (UDS + token + HMAC + TTL)
v
Mac App (UI + TCC + system.run)
system.run is controlled by Exec approvals in the macOS app (Settings → Exec approvals).
Security + ask + allowlist are stored locally on the Mac in:
~/.openclaw/exec-approvals.json
Example:
{
"version": 1,
"defaults": {
"security": "deny",
"ask": "on-miss"
},
"agents": {
"main": {
"security": "allowlist",
"ask": "on-miss",
"allowlist": [{ "pattern": "/opt/homebrew/bin/rg" }]
}
}
}
Notes:
allowlist entries are glob patterns for resolved binary paths, or bare command names for PATH-invoked commands.&&, ||, ;, |, `, $, <, >, (, )) is treated as an allowlist miss and requires explicit approval (or allowlisting the shell binary).system.run environment overrides are filtered (drops PATH, DYLD_*, LD_*, NODE_OPTIONS, NODE_REDIRECT_WARNINGS, NODE_REPL_EXTERNAL_MODULE, NODE_REPL_HISTORY, NODE_V8_COVERAGE, PYTHON*, PERL*, RUBYOPT, SHELLOPTS, PS4) and then merged with the app's environment.bash|sh|zsh ... -c/-lc), request-scoped environment overrides are reduced to a small explicit allowlist (TERM, LANG, LC_*, COLORTERM, NO_COLOR, FORCE_COLOR).env, nice, nohup, stdbuf, timeout) persist inner executable paths instead of wrapper paths. If unwrapping is not safe, no allowlist entry is persisted automatically.The app registers the openclaw:// URL scheme for local actions.
openclaw://agentTriggers a Gateway agent request.
open 'openclaw://agent?message=Hello%20from%20deep%20link'
Query parameters:
message (required)sessionKey (optional)thinking (optional)deliver / to / channel (optional)timeoutSeconds (optional)key (optional unattended mode key)Safety:
key, the app prompts for confirmation.key, the app enforces a short message limit for the confirmation prompt and ignores deliver / to / channel.key, the run is unattended (intended for personal automations).Avoid putting your OpenClaw state dir in iCloud or other cloud-synced folders. Sync-backed paths can add latency and occasionally cause file-lock/sync races for sessions and credentials.
Prefer a local non-synced state path such as:
OPENCLAW_STATE_DIR=~/.openclaw
If openclaw doctor detects state under:
~/Library/Mobile Documents/com~apple~CloudDocs/...~/Library/CloudStorage/...it will warn and recommend moving back to a local path.
cd apps/macos && swift buildswift run OpenClaw (or Xcode)scripts/package-mac-app.shUse the debug CLI to exercise the same Gateway WebSocket handshake and discovery logic that the macOS app uses, without launching the app.
cd apps/macos
swift run openclaw-mac connect --json
swift run openclaw-mac discover --timeout 3000 --json
Connect options:
--url <ws://host:port>: override config--mode <local|remote>: resolve from config (default: config or local)--probe: force a fresh health probe--timeout <ms>: request timeout (default: 15000)--json: structured output for diffingDiscovery options:
--include-local: include gateways that would be filtered as "local"--timeout <ms>: overall discovery window (default: 2000)--json: structured output for diffingWhen the macOS app runs in Remote mode, it opens an SSH tunnel so local UI components can talk to a remote Gateway as if it were on localhost.
18789), always stable.ssh -N -L <local>:127.0.0.1:<remote> with BatchMode +
ExitOnForwardFailure + keepalive options.127.0.0.1. Use Direct (ws/wss) transport if you want the real client
IP to appear (see macOS remote access).For setup steps, see macOS remote access. For protocol details, see Gateway protocol.