On this page
On this page
macOS companion app
macOS IPC
OpenClaw macOS IPC architecture
A local Unix socket connects the node host service to the macOS app for exec approvals and system.run. The bundled openclaw-mac CLI uses a separate local control socket to inspect and configure primary and saved Gateway connections. Agent actions still flow through the Gateway WebSocket and node.invoke. The node-backed computer.act path runs embedded Peekaboo automation in-process. Standalone Peekaboo clients use PeekabooBridge.
Goals
- Single GUI app instance that owns all TCC-facing work (notifications, screen recording, mic, speech, AppleScript).
- A small surface for automation: Gateway + node commands, in-process
computer.act, plus PeekabooBridge for standalone UI automation clients. - Predictable permissions: always the same signed bundle ID, launched by launchd, so TCC grants stick.
How it works
Gateway + node transport
- The app runs the Gateway (local mode) and connects to it as a node.
- Agent actions are performed via
node.invoke(e.g.system.run,system.notify,canvas.present). - Node commands include
canvas.present,canvas.hide,canvas.navigate,camera.list,camera.snap,camera.clip,camera.ptz.status,camera.ptz.control,screen.snapshot,screen.record,computer.act,system.run, andsystem.notify. - The node reports a
permissionsmap. Agents use it to see whether screen, camera, microphone, speech, automation, or accessibility access is available.
Node service + app IPC
- A headless node host service connects to the Gateway WebSocket.
system.runrequests are forwarded to the macOS app over a local Unix socket (ExecApprovalsSocket.swift).- The app performs the exec in UI context, prompts if needed, and returns output.
- The socket owns the request lifetime. Node cancellation or a socket deadline closes the response reader, cancelling the native prompt or command and its process group. Stopping the app's exec server also cancels and drains active requests before releasing its socket lease.
- Clients still half-close their write side after sending one JSONL request. That normal request EOF does not cancel execution. The response reader remains open until the result arrives.
The exec response deadline includes the requested command timeoutMs, the
companion's 15-second approval window, and 5 seconds for transport overhead.
An omitted timeout keeps the 20-second default; timeoutMs: 0 disables the
response deadline while preserving cancellation. After submission, a response
deadline reports TIMEOUT and a lost or oversized reply reports UNKNOWN.
Both mean the execution outcome is unknown; the command is never replayed
locally. COMPANION_APP_UNAVAILABLE is reserved for requests that could not be
submitted to the companion.
The app obtains shared exec policy through the local CLI's bounded owner operations: read a redacted snapshot, replace policy with the expected hash, and commit a bound execution decision with any allow-always grant and usage update. The CLI routes to the running shared-state owner or acquires exclusive offline custody. This is the same path for SQLite and PostgreSQL; Swift never opens the shared policy database. With PostgreSQL selected, the local SQLite engine anchor contains no policy tables. Socket credentials remain in the separate local credential owner described below.
The final owner commit checks current policy, including revocations made while a prompt was open and agent-deletion fences. The app awaits that commit within the original socket request, then checks cancellation and revalidates the bound executable and working directory immediately before spawn. A UI policy snapshot does not grant execution, and no database transaction stays open across the IPC wait. Owner failure, a missing operation, or PostgreSQL unavailability returns a visible policy-unavailable denial. Offline custody still requires the selected database to be reachable; there is no local policy fallback.
Per-node policy reads and edits still use exec.approvals.node.get/set, which
route system.execApprovals.get/set to the selected node. Moving the macOS
policy access behind its local owner does not redirect it to a remote Gateway's
policy.
Diagram (SCI):
App control socket
openclaw-mac sends one JSONL request and receives one JSONL response per
Unix-domain connection. The running app starts and stops this listener with
its exec-approvals lifecycle owner. The shared listener owns the socket path
lease, guarded filesystem cleanup, connection framing, and shutdown drainage.
It exposes only these operations:
| Operation | Result and effect |
|---|---|
status |
Primary connection, saved Gateways, and app version/build/profile. |
primary.set |
Set local mode, SSH transport, or direct transport through the app's native connection owners; return the resulting primary status. |
primary.clear |
Return the primary connection to its unconfigured state; return primary status. |
gateway.list |
List saved Gateways and their connection status. |
gateway.add |
Validate and connect through the Gateways tab's sign-in coordinator; return the saved Gateway and identity when present. |
gateway.remove |
Resolve an ID/name and use the profile store's Remove path, including credential and dashboard-data cleanup. |
gateway.reconnect |
Repeat browser sign-in for browser profiles or reconnect the saved token/password connection. |
client.credentials |
Internal bounded access to the app-owned local credential store for an explicitly configured client root. |
Requests identify the operation with an operation string. Primary SSH
settings include sshTarget, optional ports, identityPath, and
hostKeyPolicy (strict or openssh). Direct settings include url and an
optional tlsFingerprint. Credential-bearing operations accept token or
password inside the authenticated request. The CLI reads these from files
or stdin. Saved-Gateway requests use name, url, and browser, or
idOrName for removal/reconnection. Matching tries an exact ID first, then a
unique case-insensitive name. Ambiguity is an error with candidate names/IDs.
The default profile uses ~/.openclaw/mac-control.sock and
~/.openclaw/mac-control.token. Named profiles use the corresponding
~/.openclaw-<name>/ directory. These paths follow
AppProfile.stateDirectoryURL, independently of config/state environment
overrides. The app owns token creation. The token must be a regular,
user-owned file with mode 0600. Missing or unsafe credentials reject
requests. Clients must have the app's peer UID (getpeereid). The JSON
envelope carries a nonce, millisecond timestamp, encoded request, and
HMAC-SHA256 over the nonce, timestamp, and exact request bytes. Requests have a
15-second authentication TTL. A successfully authenticated browser sign-in
may remain open for the coordinator's bounded 300-second operation.
Socket responses use {ok:true,result:...} or
{ok:false,error:{code,message}}. The CLI's --json success output unwraps
result. status contains:
primary: mode, nullable transport, URL, optional SSH target/remote port, tunnel state/local port, and connection state/version/error.gateways: ID, name, URL, authentication kind (token,password, orbrowser), optional Cloudflare Access identity subject/expiry, and connection state/error.app: version, build, and profile.
Public setup responses never include tokens, passwords, browser-session credentials,
or Keychain contents. The internal client.credentials operation returns the
requested local credential only over this authenticated same-UID transport. It
accepts the app's configured identity and exec-host roots, never an arbitrary
database path. A lost response after dispatch does not permit an offline replay.
Saved-Gateway mutations execute inside the signed app,
where the profile store owns Keychain access, connection retirement,
dashboard cookies, and device-token cleanup. The CLI never writes the
saved-Gateway registry.
The app retains the operation route for its configured roots. Each operation
acquires exclusive custody of client/credentials.sqlite and closes it before
returning. Other clients contact the app first; only an undispatched request can
use the app-absent route. Existing maintenance admission blocks new app operations
while updater or Doctor custody captures, migrates, or restores the local store.
The same rule applies when a named profile uses one root for both identity and
execution. A separate Application Support identity root remains independently
owned throughout bootstrap.
Native identity bootstrap uses the existing Application Support root; exec-host
secrets use the existing resolved state root. Neither opens the selected Gateway
database to load private client material. Older apps cannot use retired shared credential tables: update the app
and run the matching openclaw doctor --fix before reconnecting. The existing
mac-control.token bootstrap file and its transport authentication stay unchanged.
The shared-policy owner cutover needs no schema migration. An older app that is still admitted on SQLite sees the same policy row, but cannot access PostgreSQL policy. A new app requires the matching local CLI and owner operation; an older CLI or Gateway fails with a diagnostic instead of triggering direct database access. Roll back only to an app/CLI combination compatible with the current policy format, without restoring an older policy snapshot or rewinding local credentials.
See remote control for shell examples, profile selection, safe credential input, and background app launch.
PeekabooBridge (UI automation)
- The built-in agent
computertool does not use this socket. A paired macOS node fulfillscomputer.actin the app process with embedded Peekaboo services. - UI automation uses a separate UNIX socket (
~/Library/Application Support/OpenClaw/<socket>) and the PeekabooBridge JSON protocol. - Host preference order (client-side): Peekaboo.app -> Claude.app -> OpenClaw.app -> local execution.
- Security: bridge hosts require the exact signed Peekaboo client bundle identifier and Peekaboo's official
current/legacy release signer set.
PEEKABOO_ALLOW_UNSIGNED_SOCKET_CLIENTS=1guards a DEBUG-only same-UID escape hatch (Peekaboo convention). - See: PeekabooBridge usage for details.
Operational flows
- Restart/rebuild:
scripts/restart-mac.shkills existing instances, rebuilds via Swift, repackages, and relaunches. It auto-detects an available signing identity. It falls back to--no-signif it finds no identity. Pass--signto require signing, which fails if no key is available. Pass--no-signto force the unsigned path. Packaging preserves an explicitSIGN_IDENTITY. Otherwisescripts/codesign-mac-app.shauto-detects the certificate. - Single instance: the app checks
NSWorkspace.runningApplicationsfor a duplicate bundle ID. It exits if it finds more than one instance (isDuplicateInstance()inMenuBar.swift).
Hardening notes
- Prefer requiring a TeamID match for all privileged surfaces.
- PeekabooBridge:
PEEKABOO_ALLOW_UNSIGNED_SOCKET_CLIENTS=1(DEBUG-only) may allow same-UID callers for local development. - All communication remains local-only. No network sockets are exposed.
- TCC prompts originate only from the GUI app bundle. Keep the signed bundle ID stable across rebuilds.
- Exec approvals socket hardening: file mode
0600, local token stored inclient/credentials.sqliteunder the resolved client state root, peer-UID check (getpeereid), HMAC-SHA256 challenge/response, and a short TTL on requests.