#!/usr/bin/env bash
set -euo pipefail
APP_BUNDLE=
"${1:-dist/OpenClaw.app}"
IDENTITY=
"${SIGN_IDENTITY:-}"
TIMESTAMP_MODE=
"${CODESIGN_TIMESTAMP:-auto}"
DISABLE_LIBRARY_VALIDATION=
"${DISABLE_LIBRARY_VALIDATION:-0}"
SKIP_TEAM_ID_CHECK=
"${SKIP_TEAM_ID_CHECK:-0}"
ENT_TMP_BASE=$(mktemp -t openclaw-entitlements-base.XXXXXX)
ENT_TMP_APP_BASE=$(mktemp -t openclaw-entitlements-app-base.XXXXXX)
ENT_TMP_RUNTIME=$(mktemp -t openclaw-entitlements-runtime.XXXXXX)
if [[
"${APP_BUNDLE}" ==
"--help" ||
"${APP_BUNDLE}" ==
"-h" ]];
then
cat <<
'HELP'
Usage: scripts/codesign-mac-app.sh [app-bundle]
Env:
SIGN_IDENTITY=
"Apple Development: Your Name (TEAMID)"
ALLOW_ADHOC_SIGNING=1
CODESIGN_TIMESTAMP=auto|on|off
DISABLE_LIBRARY_VALIDATION=1
# dev-only Sparkle Team ID workaround
SKIP_TEAM_ID_CHECK=1
# bypass Team ID audit
HELP
exit 0
fi
if [ ! -d
"$APP_BUNDLE" ];
then
echo "App bundle not found: $APP_BUNDLE" >&2
exit 1
fi
select_identity() {
local preferred available first
# Prefer a Developer ID Application cert.
preferred=
"$(security find-identity -p codesigning -v 2>/dev/null \
| awk -F
'\"' '/Developer ID Application/ { print $2; exit }')
"
if [ -n
"$preferred" ];
then
echo "$preferred"
return
fi
# Next, try Apple Distribution.
preferred=
"$(security find-identity -p codesigning -v 2>/dev/null \
| awk -F
'\"' '/Apple Distribution/ { print $2; exit }')
"
if [ -n
"$preferred" ];
then
echo "$preferred"
return
fi
# Then, try Apple Development.
preferred=
"$(security find-identity -p codesigning -v 2>/dev/null \
| awk -F
'\"' '/Apple Development/ { print $2; exit }')
"
if [ -n
"$preferred" ];
then
echo "$preferred"
return
fi
# Fallback to the first valid signing identity.
available=
"$(security find-identity -p codesigning -v 2>/dev/null \
| sed -n
's/.*\"\\(.*\\)\"/\\1/p')
"
if [ -n
"$available" ];
then
first=
"$(printf '%s\n' "$available
" | head -n1)"
echo "$first"
return
fi
return 1
}
if [ -z
"$IDENTITY" ];
then
if ! IDENTITY=
"$(select_identity)";
then
if [[
"${ALLOW_ADHOC_SIGNING:-}" ==
"1" ]];
then
echo "WARN: No signing identity found. Falling back to ad-hoc signing (-)." >&2
echo " !!! WARNING: Ad-hoc signed apps do NOT persist TCC permissions (Accessibility, etc) !!!" >&2
echo " !!! You will need to re-grant permissions every time you restart the app. !!!" >&2
IDENTITY=
"-"
else
echo "ERROR: No signing identity found. Set SIGN_IDENTITY to a valid codesigning certificate." >&2
echo " Alternatively, set ALLOW_ADHOC_SIGNING=1 to fallback to ad-hoc signing (limitations apply)." >&2
exit 1
fi
fi
fi
echo "Using signing identity: $IDENTITY"
if [[
"$IDENTITY" ==
"-" ]];
then
cat <<
'WARN' >&2
================================================================================
!!! AD-HOC SIGNING IN USE - PERMISSIONS WILL NOT STICK (macOS RESTRICTION) !!!
macOS ties permissions to the code signature, bundle ID, and app path.
Ad-hoc signing generates a new signature every build, so macOS treats the app
as a different binary and will forget permissions (prompts may vanish).
For correct permission behavior you MUST sign with a real Apple Development or
Developer ID certificate.
If prompts disappear: remove the app entry in System Settings -> Privacy & Security,
relaunch the app, and re-grant. Some permissions only reappear after a full
macOS restart.
================================================================================
WARN
fi
timestamp_arg=
"--timestamp=none"
case
"$TIMESTAMP_MODE" in
1|on|yes|true)
timestamp_arg=
"--timestamp"
;;
0|off|no|false)
timestamp_arg=
"--timestamp=none"
;;
auto)
if [[
"$IDENTITY" == *
"Developer ID Application"* ]];
then
timestamp_arg=
"--timestamp"
fi
;;
*)
echo "ERROR: Unknown CODESIGN_TIMESTAMP value: $TIMESTAMP_MODE (use auto|on|off)" >&2
exit 1
;;
esac
if [[
"$IDENTITY" ==
"-" ]];
then
timestamp_arg=
"--timestamp=none"
fi
options_args=()
if [[
"$IDENTITY" !=
"-" ]];
then
options_args=(
"--options" "runtime")
fi
timestamp_args=(
"$timestamp_arg")
cat >
"$ENT_TMP_BASE" <<
'PLIST'
<?xml version=
"1.0" encoding=
"UTF-8"?>
<!DOCTYPE plist PUBLIC
"-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version=
"1.0">
<dict>
<key>com.apple.security.automation.apple-events</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>
PLIST
cat >
"$ENT_TMP_APP_BASE" <<
'PLIST'
<?xml version=
"1.0" encoding=
"UTF-8"?>
<!DOCTYPE plist PUBLIC
"-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version=
"1.0">
<dict>
<key>com.apple.security.automation.apple-events</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
</dict>
</plist>
PLIST
cat >
"$ENT_TMP_RUNTIME" <<
'PLIST'
<?xml version=
"1.0" encoding=
"UTF-8"?>
<!DOCTYPE plist PUBLIC
"-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version=
"1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>
PLIST
if [[
"$DISABLE_LIBRARY_VALIDATION" ==
"1" ]];
then
/usr/libexec/PlistBuddy -c
"Add :com.apple.security.cs.disable-library-validation bool true" "$ENT_TMP_APP_BASE" >/dev/null 2>&1 || \
/usr/libexec/PlistBuddy -c
"Set :com.apple.security.cs.disable-library-validation true" "$ENT_TMP_APP_BASE"
echo "Note: disable-library-validation entitlement enabled (DISABLE_LIBRARY_VALIDATION=1)."
fi
APP_ENTITLEMENTS=
"$ENT_TMP_APP_BASE"
# clear extended attributes to avoid stale signatures
xattr -cr
"$APP_BUNDLE" 2>/dev/null || true
sign_item() {
local target=
"$1"
local entitlements=
"$2"
codesign --force ${options_args+
"${options_args[@]}"}
"${timestamp_args[@]}" --entit
lements "$entitlements" --sign "$IDENTITY" "$target"
}
sign_plain_item() {
local target="$1"
codesign --force ${options_args+"${options_args[@]}"} "${timestamp_args[@]}" --sign "$IDENTITY" "$target"
}
team_id_for() {
codesign -dv --verbose=4 "$1" 2>&1 | awk -F= '/^TeamIdentifier=/{print $2; exit}'
}
verify_team_ids() {
if [[ "$SKIP_TEAM_ID_CHECK" == "1" ]]; then
echo "Note: skipping Team ID audit (SKIP_TEAM_ID_CHECK=1)."
return 0
fi
local expected
expected="$(team_id_for "$APP_BUNDLE" || true)"
if [[ -z "$expected" ]]; then
echo "WARN: TeamIdentifier missing on app bundle; skipping Team ID audit."
return 0
fi
local mismatches=()
while IFS= read -r -d '' f; do
if /usr/bin/file "$f" | /usr/bin/grep -q "Mach-O"; then
local team
team="$(team_id_for "$f" || true)"
if [[ -z "$team" ]]; then
team="not set"
fi
if [[ "$expected" == "not set" ]]; then
if [[ "$team" != "not set" ]]; then
mismatches+=("$f (TeamIdentifier=$team)")
fi
elif [[ "$team" != "$expected" ]]; then
mismatches+=("$f (TeamIdentifier=$team)")
fi
fi
done < <(find "$APP_BUNDLE" -type f -print0)
if [[ "${#mismatches[@]}" -gt 0 ]]; then
echo "ERROR: Team ID mismatch detected (expected: $expected)"
for entry in "${mismatches[@]}"; do
echo " - $entry"
done
echo "Hint: re-sign embedded frameworks or set DISABLE_LIBRARY_VALIDATION=1 for dev builds."
exit 1
fi
}
# Sign bundled helper binaries before signing the app bundle.
MLX_TTS_HELPER="$APP_BUNDLE/Contents/MacOS/openclaw-mlx-tts"
if [ -f "$MLX_TTS_HELPER" ]; then
echo "Signing MLX TTS helper"; sign_item "$MLX_TTS_HELPER" "$APP_ENTITLEMENTS"
fi
# Sign main binary
if [ -f "$APP_BUNDLE/Contents/MacOS/OpenClaw" ]; then
echo "Signing main binary"; sign_item "$APP_BUNDLE/Contents/MacOS/OpenClaw" "$APP_ENTITLEMENTS"
fi
# Sign Sparkle deeply if present
SPARKLE="$APP_BUNDLE/Contents/Frameworks/Sparkle.framework"
if [ -d "$SPARKLE" ]; then
echo "Signing Sparkle framework and helpers"
find "$SPARKLE" -type f -print0 | while IFS= read -r -d '' f; do
if /usr/bin/file "$f" | /usr/bin/grep -q "Mach-O"; then
sign_plain_item "$f"
fi
done
sign_plain_item "$SPARKLE/Versions/B/Sparkle"
sign_plain_item "$SPARKLE/Versions/B/Autoupdate"
sign_plain_item "$SPARKLE/Versions/B/Updater.app/Contents/MacOS/Updater"
sign_plain_item "$SPARKLE/Versions/B/Updater.app"
sign_plain_item "$SPARKLE/Versions/B/XPCServices/Downloader.xpc/Contents/MacOS/Downloader"
sign_plain_item "$SPARKLE/Versions/B/XPCServices/Downloader.xpc"
sign_plain_item "$SPARKLE/Versions/B/XPCServices/Installer.xpc/Contents/MacOS/Installer"
sign_plain_item "$SPARKLE/Versions/B/XPCServices/Installer.xpc"
sign_plain_item "$SPARKLE/Versions/B"
sign_plain_item "$SPARKLE"
fi
# Sign any other embedded frameworks/dylibs
if [ -d "$APP_BUNDLE/Contents/Frameworks" ]; then
find "$APP_BUNDLE/Contents/Frameworks" \( -name "*.framework" -o -name "*.dylib" \) ! -path "*Sparkle.framework*" -print0 | while IFS= read -r -d '' f; do
echo "Signing framework: $f"; sign_plain_item "$f"
done
fi
# Finally sign the bundle
sign_item "$APP_BUNDLE" "$APP_ENTITLEMENTS"
verify_team_ids
rm -f "$ENT_TMP_BASE" "$ENT_TMP_APP_BASE" "$ENT_TMP_RUNTIME"
echo "Codesign complete for $APP_BUNDLE"