diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 0213ccfe..9f20fe61 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -201,9 +201,61 @@ jobs:
zip code-assistant-${{ matrix.name }}.zip "code-assistant${{ matrix.extension }}"
fi
+ - name: Set up macOS code signing
+ if: matrix.os == 'macos-latest'
+ shell: bash
+ env:
+ MACOS_CERT_P12_BASE64: ${{ secrets.MACOS_CERT_P12_BASE64 }}
+ MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
+ KEYCHAIN_PASSWORD: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}
+ run: |
+ # Skip silently when no certificate is configured: the bundle script
+ # then falls back to ad-hoc signing and the release still builds.
+ if [ -z "$MACOS_CERT_P12_BASE64" ]; then
+ echo "No MACOS_CERT_P12_BASE64 secret set; skipping code-signing setup."
+ exit 0
+ fi
+
+ KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
+ CERT_PATH="$RUNNER_TEMP/certificate.p12"
+
+ # Materialize the base64-encoded .p12 certificate.
+ echo -n "$MACOS_CERT_P12_BASE64" | base64 --decode > "$CERT_PATH"
+
+ # Create a dedicated, ephemeral keychain and make it the default so
+ # codesign can find the identity non-interactively.
+ security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
+ security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
+ security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
+
+ # Import the Developer ID Application certificate + private key.
+ security import "$CERT_PATH" \
+ -P "$MACOS_CERT_PASSWORD" \
+ -A -t cert -f pkcs12 \
+ -k "$KEYCHAIN_PATH"
+
+ # Allow codesign/productsign to use the key without a UI prompt.
+ security set-key-partition-list \
+ -S apple-tool:,apple:,codesign: \
+ -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
+
+ # Put our keychain in the search list so codesign sees the identity.
+ security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain-db
+ security default-keychain -s "$KEYCHAIN_PATH"
+
+ echo "Available signing identities:"
+ security find-identity -v -p codesigning "$KEYCHAIN_PATH"
+
+ rm -f "$CERT_PATH"
+
- name: Build macOS .app bundle
if: matrix.os == 'macos-latest'
shell: bash
+ env:
+ MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }}
+ MACOS_NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }}
+ MACOS_NOTARY_PASSWORD: ${{ secrets.MACOS_NOTARY_PASSWORD }}
+ MACOS_NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }}
run: |
# Map matrix target -> bundle script arch argument
case "${{ matrix.target }}" in
diff --git a/crates/code_assistant/assets/Entitlements.plist b/crates/code_assistant/assets/Entitlements.plist
new file mode 100644
index 00000000..f81a8ce3
--- /dev/null
+++ b/crates/code_assistant/assets/Entitlements.plist
@@ -0,0 +1,25 @@
+
+
+
+
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+
+
diff --git a/docs/macos-signing.md b/docs/macos-signing.md
new file mode 100644
index 00000000..7c5feece
--- /dev/null
+++ b/docs/macos-signing.md
@@ -0,0 +1,94 @@
+# macOS Code Signing & Notarization
+
+The macOS `.app` bundle is produced by `scripts/bundle-macos.sh`. The script is
+usable in three ways, driven entirely by environment variables — it has no
+CI-specific logic:
+
+| Scenario | What happens |
+| --- | --- |
+| No env vars set (default, e.g. a local build) | **Ad-hoc** signature only. No Apple account needed. Not notarized. |
+| `MACOS_SIGN_IDENTITY` set | Signs with your **Developer ID Application** identity, hardened runtime + entitlements. |
+| Signing identity **and** all notary vars set | Additionally **notarizes** with `xcrun notarytool` and **staples** the ticket. |
+
+All signing uses only tools shipping with macOS / Xcode (`codesign`,
+`xcrun notarytool`, `xcrun stapler`) — no third-party actions or CLIs.
+
+## Local usage
+
+### Plain build (no signing)
+
+```bash
+./scripts/bundle-macos.sh # host arch, ad-hoc signed
+```
+
+### Signed build with your own certificate
+
+If you have a *Developer ID Application* certificate in your login keychain:
+
+```bash
+# Find the exact identity string:
+security find-identity -v -p codesigning
+
+export MACOS_SIGN_IDENTITY="Developer ID Application: Jane Doe (TEAMID1234)"
+./scripts/bundle-macos.sh
+```
+
+### Signed + notarized build
+
+Add the notary credentials (an app-specific password created at
+):
+
+```bash
+export MACOS_SIGN_IDENTITY="Developer ID Application: Jane Doe (TEAMID1234)"
+export MACOS_NOTARY_APPLE_ID="jane@example.com"
+export MACOS_NOTARY_PASSWORD="abcd-efgh-ijkl-mnop" # app-specific password
+export MACOS_NOTARY_TEAM_ID="TEAMID1234"
+./scripts/bundle-macos.sh universal
+```
+
+## CI usage
+
+The `Release` workflow (`.github/workflows/release.yml`) does the same thing
+automatically on the macOS build jobs. It imports the certificate into an
+ephemeral keychain and then calls the exact same bundle script. When the secrets
+are **not** configured, the workflow falls back to ad-hoc signing and the
+release still succeeds.
+
+### Required GitHub secrets
+
+Configure these under **Settings → Secrets and variables → Actions**:
+
+| Secret | Purpose |
+| --- | --- |
+| `MACOS_CERT_P12_BASE64` | Your *Developer ID Application* certificate **and private key** exported as a `.p12`, then base64-encoded. |
+| `MACOS_CERT_PASSWORD` | The password protecting that `.p12` file. |
+| `MACOS_KEYCHAIN_PASSWORD` | Any throwaway password used for the temporary keychain created on the runner. |
+| `MACOS_SIGN_IDENTITY` | The identity name, e.g. `Developer ID Application: Jane Doe (TEAMID1234)`. |
+| `MACOS_NOTARY_APPLE_ID` | Apple ID email used for notarization. |
+| `MACOS_NOTARY_PASSWORD` | App-specific password for that Apple ID. |
+| `MACOS_NOTARY_TEAM_ID` | Apple Developer Team ID (10 characters). |
+
+If only the signing secrets (first four) are set, the app is signed but not
+notarized. If none are set, the build is ad-hoc signed.
+
+### Producing the certificate secrets
+
+1. In Keychain Access, export your *Developer ID Application* certificate
+ (including its private key) as a `.p12` file and set a password.
+2. Base64-encode it for storage in a secret:
+
+ ```bash
+ base64 -i DeveloperID.p12 | pbcopy # now paste into MACOS_CERT_P12_BASE64
+ ```
+
+3. Put the `.p12` password into `MACOS_CERT_PASSWORD`.
+
+## Entitlements
+
+Signing under the hardened runtime uses
+`crates/code_assistant/assets/Entitlements.plist`. Because the app embeds gpui
+(Zed's UI framework), the entitlements allow JIT / unsigned executable memory,
+which the hardened runtime otherwise blocks at launch. The set mirrors the
+hardened-runtime keys used by Zed itself and deliberately avoids the weaker keys
+(library-validation / dyld-environment / executable-page-protection) that Zed
+also leaves off and that Apple's notary service scrutinizes.
diff --git a/scripts/bundle-macos.sh b/scripts/bundle-macos.sh
index c363cf81..a6c62b33 100755
--- a/scripts/bundle-macos.sh
+++ b/scripts/bundle-macos.sh
@@ -2,7 +2,7 @@
# Build a macOS .app bundle for Code Assistant.
#
# Uses the static Info.plist from assets/ and replaces only the version.
-# Only relies on tools that ship with macOS (plutil, codesign).
+# Only relies on tools that ship with macOS / Xcode (plutil, codesign, xcrun).
#
# Usage:
# ./scripts/bundle-macos.sh # build for the host arch
@@ -10,6 +10,17 @@
#
# ARCH can be: aarch64, x86_64, universal
#
+# Code signing & notarization (all optional, via environment variables):
+# MACOS_SIGN_IDENTITY Developer ID Application identity to sign with,
+# e.g. "Developer ID Application: Jane Doe (TEAMID)".
+# If unset, the bundle is ad-hoc signed (as before).
+#
+# Notarization runs only when ALL of these are set (and signing happened
+# with a real identity):
+# MACOS_NOTARY_APPLE_ID Apple ID email used for notarization.
+# MACOS_NOTARY_PASSWORD App-specific password for that Apple ID.
+# MACOS_NOTARY_TEAM_ID Apple Developer Team ID.
+#
# Output:
# target/macos-bundle/Code Assistant.app
# target/macos-bundle/Code-Assistant--.zip
@@ -32,7 +43,7 @@ for arg in "$@"; do
aarch64|arm64) ARCH="aarch64" ;;
x86_64|intel) ARCH="x86_64" ;;
universal) ARCH="universal" ;;
- -h|--help) sed -n '2,16p' "$0"; exit 0 ;;
+ -h|--help) sed -n '2,32p' "$0"; exit 0 ;;
*) echo "error: unknown argument: $arg" >&2; exit 1 ;;
esac
done
@@ -134,10 +145,33 @@ printf 'APPL????' > "$CONTENTS/PkgInfo"
touch "$APP_DIR"
# ---------------------------------------------------------------------------
-# Ad-hoc code signature
+# Code signature
# ---------------------------------------------------------------------------
-if command -v codesign >/dev/null 2>&1; then
- echo "==> Ad-hoc signing"
+# When MACOS_SIGN_IDENTITY is provided we sign with a Developer ID identity,
+# enabling the hardened runtime and applying our entitlements (required for
+# notarization). Otherwise we fall back to an ad-hoc signature, which is enough
+# for local use but cannot be notarized or distributed without Gatekeeper
+# warnings.
+ENTITLEMENTS="$ASSETS_DIR/Entitlements.plist"
+
+if ! command -v codesign >/dev/null 2>&1; then
+ echo "==> codesign not available; skipping signature"
+elif [[ -n "${MACOS_SIGN_IDENTITY:-}" ]]; then
+ echo "==> Signing with Developer ID: $MACOS_SIGN_IDENTITY"
+ # Sign the inner executable first, then the bundle (deep), all under the
+ # hardened runtime and with a secure timestamp (required by notarization).
+ codesign --force --timestamp --options runtime \
+ --entitlements "$ENTITLEMENTS" \
+ --sign "$MACOS_SIGN_IDENTITY" \
+ "$CONTENTS/MacOS/$EXECUTABLE_NAME"
+ codesign --force --deep --timestamp --options runtime \
+ --entitlements "$ENTITLEMENTS" \
+ --sign "$MACOS_SIGN_IDENTITY" \
+ "$APP_DIR"
+ echo "==> Verifying signature"
+ codesign --verify --deep --strict --verbose=2 "$APP_DIR"
+else
+ echo "==> No MACOS_SIGN_IDENTITY set; ad-hoc signing"
codesign --force --deep --sign - "$APP_DIR" >/dev/null 2>&1 || true
fi
@@ -145,8 +179,40 @@ fi
# Zip for distribution
# ---------------------------------------------------------------------------
ZIP_PATH="$OUT_DIR/Code-Assistant-$VERSION-$ARCH.zip"
-rm -f "$ZIP_PATH"
-( cd "$OUT_DIR" && zip -qry "$ZIP_PATH" "Code Assistant.app" )
+make_zip() {
+ rm -f "$ZIP_PATH"
+ # ditto preserves the bundle structure and extended attributes; notarytool
+ # requires a ditto/zip archive of the .app.
+ ( cd "$OUT_DIR" && ditto -c -k --keepParent "Code Assistant.app" "$ZIP_PATH" )
+}
+make_zip
+
+# ---------------------------------------------------------------------------
+# Notarization + stapling
+# ---------------------------------------------------------------------------
+# Only attempt notarization when we signed with a real identity and all notary
+# credentials are present.
+if [[ -n "${MACOS_SIGN_IDENTITY:-}" &&
+ -n "${MACOS_NOTARY_APPLE_ID:-}" &&
+ -n "${MACOS_NOTARY_PASSWORD:-}" &&
+ -n "${MACOS_NOTARY_TEAM_ID:-}" ]]; then
+ echo "==> Submitting to Apple notary service (this can take a few minutes)"
+ xcrun notarytool submit "$ZIP_PATH" \
+ --apple-id "$MACOS_NOTARY_APPLE_ID" \
+ --password "$MACOS_NOTARY_PASSWORD" \
+ --team-id "$MACOS_NOTARY_TEAM_ID" \
+ --wait
+
+ echo "==> Stapling notarization ticket to the .app"
+ xcrun stapler staple "$APP_DIR"
+ xcrun stapler validate "$APP_DIR"
+
+ # Re-zip so the distributed archive contains the stapled bundle.
+ echo "==> Re-packaging stapled bundle"
+ make_zip
+else
+ echo "==> Skipping notarization (identity and/or notary credentials not set)"
+fi
echo
echo "==> Done:"