macOS companion app

macOS signing

mac signing (debug builds)

scripts/package-mac-app.sh builds a staged app, calls scripts/codesign-mac-app.sh, and verifies the signed worker before replacing dist/OpenClaw.app. macOS ties TCC permissions to the bundle ID and code signature. Keep both stable across rebuilds, and keep the app at a fixed path. macOS then keeps its TCC grants (notifications, accessibility, screen recording, mic, speech).

  • Debug bundle identifier defaults to ai.openclaw.mac.debug (override with BUNDLE_ID=...).
  • Node: >=24.16.0 <25 or >=26.1.0 (repo package.json engines). The packager also builds the Control UI (pnpm ui:build).
  • Requires a real signing identity by default. The codesign script exits with an error if it finds no identity and ALLOW_ADHOC_SIGNING is not set. Ad-hoc signing (SIGN_IDENTITY="-") is explicit opt-in and does not persist TCC permissions across rebuilds. See macOS permissions.
  • Reads SIGN_IDENTITY from the environment (e.g. export SIGN_IDENTITY="Apple Development: Your Name (TEAMID)", or a Developer ID Application cert). Without it, codesign-mac-app.sh auto-selects an identity. The order is Developer ID Application, Apple Distribution, Apple Development, then the first valid codesigning identity found.
  • SIGN_IDENTITY also accepts a certificate SHA-1 hash to distinguish certificates with the same common name.
  • CODESIGN_TIMESTAMP=auto (default) enables trusted timestamps for Developer ID Application signatures selected by name or certificate hash. Set on/off to force either way.
  • Stamps Info.plist with OpenClawBuildTimestamp (ISO8601 UTC) and OpenClawGitCommit. The commit value is the full 40-character hexadecimal commit, or unknown for local builds when it is unavailable. The standard About panel can then show the build timestamp and git commit.
  • Audits native-signature format and Team IDs after signing. Metadata failures, non-native signatures, missing Team IDs, and mismatched Team IDs fail by default. SKIP_TEAM_ID_CHECK=1 skips only the Team ID comparison. Native-signature format checks still run.
  • Signs the private worker's native code before sealing the app. JIT memory entitlements go only to the worker's bin/node and to Claude Agent SDK claude executables. For the claude executables, this applies only when an explicitly bundled plugin includes them. The signer plain-signs other native helpers and libraries. Those signatures retain library validation and require the app's signing identity. The bundled Anthropic plugin uses the separately installed Claude Code executable. Packaging verifies each requested architecture's native capabilities and worker readiness in temporary state before and after signing.

Signing uses /usr/bin/python3 to scan file headers and batches candidates through /usr/bin/file. Apple's /usr/bin/otool then checks native headers for every architecture, including fat64 containers that file reports as data. Mixed signing categories and malformed native headers stop signing. Java classes, static archives, and Mach-O linker, debug, and core artifacts are resource-sealed rather than directly signed. Discovery opens directories without following symlinks and classifies opened file descriptors rather than redirectable paths. It does not run the bundled Node to sign itself. Before publishing, the scan checks every observed input, including directory namespaces and resources omitted from the native list. It rejects incomplete traversal and observed changes. This is not an atomic filesystem snapshot.

The signature audit also requires a native Mach-O signature. Some Apple toolchains return success while signing raw fat64 as generic data, without native entitlements. The audit rejects that result. The signer does not thin or convert input containers. Rebuild unsupported native payloads as codesign-compatible code before packaging rather than relying on generic-signature verification.

The signer seals App and Sparkle bundle owners once, after their nested code. A separate signature on a bundle's main executable would also reseal that bundle's resources. After the signer seals the app, a fresh inventory feeds the Team ID and elevation audits. Scanner or classifier failures stop signing. Worker portability checks keep each architecture's loader paths separate and reject nonportable dependencies and broken or escaping symlinks.

Sign a private staged copy that no other process modifies. The signer rejects hardlinked files and special files such as FIFOs before attribute cleanup. Copy or rebuild the bundle if this check fails. Attribute cleanup and signing use macOS /usr/bin/sandbox-exec. It restricts filesystem writes to the fixed app and temporary signing directories. Extra inherited file descriptors are closed. Signing fails if this mutation boundary cannot start. There is no unrestricted fallback.

Usage

bash
# from repo rootscripts/package-mac-app.sh                                                      # auto-selects identity; errors if none foundSIGN_IDENTITY="Developer ID Application: Your Name" scripts/package-mac-app.sh   # real certALLOW_ADHOC_SIGNING=1 scripts/package-mac-app.sh                                 # ad-hoc (permissions will not stick)SIGN_IDENTITY="-" scripts/package-mac-app.sh                                     # explicit ad-hoc (same caveat)DISABLE_LIBRARY_VALIDATION=1 scripts/package-mac-app.sh                          # dev-only Sparkle Team ID mismatch workaround

Ad-hoc signing note

SIGN_IDENTITY="-" disables the Hardened Runtime (--options runtime). This prevents crashes when the app loads embedded frameworks (like Sparkle) that do not share the same Team ID. Ad-hoc signatures also break TCC permission persistence. See macOS permissions for recovery steps.

Build metadata for About

Choose About OpenClaw to open the standard macOS About panel. It shows the app version and build, with the build timestamp and git commit from Info.plist in its credits. The panel shows missing or invalid timestamp and commit values as unavailable. Re-run the packager after code changes to refresh these values. App update controls live under Dashboard → Settings → Updates → This Mac.

Was this useful?
On this page

On this page