Code Signing
Sign your LightShell app for trusted distribution on macOS.
Code signing tells macOS that your app comes from a known developer and has not been tampered with. Without a signature, macOS Gatekeeper blocks your app and users see an “unidentified developer” warning. Signing eliminates this friction. Notarization goes one step further — Apple scans your app and confirms it is free of malicious content, removing all security warnings.
Why Sign Your App
Section titled “Why Sign Your App”Unsigned apps trigger Gatekeeper on macOS:
“MyApp” can’t be opened because Apple cannot check it for malicious software.
Users must right-click and select “Open” to bypass this, which is confusing and reduces trust. A signed app opens immediately with no warnings. A signed and notarized app opens with no warnings on any macOS version, including the latest.
If you are distributing your app to anyone beyond yourself, signing is strongly recommended.
Requirements
Section titled “Requirements”To sign a macOS app, you need:
- An Apple Developer account — enroll at developer.apple.com ($99/year for individuals)
- A Developer ID Application certificate — create one in your Apple Developer account under Certificates, Identifiers & Profiles
- The certificate installed in your Keychain — download and double-click the certificate file to install it
Your signing identity looks like this:
Developer ID Application: Your Name (TEAMID)You can find your identity by running:
security find-identity -v -p codesigningThis lists all valid code signing identities in your Keychain.
Configuration
Section titled “Configuration”Add your signing identity to lightshell.json:
{ "name": "my-app", "version": "1.0.0", "build": { "appId": "com.example.myapp", "icon": "assets/icon.png", "mac": { "identity": "Developer ID Application: Your Name (TEAMID)" } }}Signing a Build
Section titled “Signing a Build”Use the --sign flag with any macOS build target:
# Sign a .app bundlelightshell build --sign
# Sign and package as DMGlightshell build --target dmg --signOutput:
✓ Built my-app in 1.2s → 5.1MB✓ Created MyApp.app✓ Signed MyApp.app with "Developer ID Application: Your Name (TEAMID)"✓ Packaged DMG → dist/MyApp-1.0.0.dmgWhat Happens During Signing
Section titled “What Happens During Signing”When you build with --sign, LightShell runs:
codesign --deep --force --options runtime --sign "Developer ID Application: Your Name (TEAMID)" MyApp.appThe flags:
--deepsigns the app bundle and all nested code (frameworks, helpers)--forcereplaces any existing signature--options runtimeenables the hardened runtime, which is required for notarization--signspecifies the signing identity
After signing, LightShell verifies the signature:
codesign --verify --deep --strict MyApp.appIf verification fails, the build stops and reports the error.
Entitlements
Section titled “Entitlements”Entitlements declare what system capabilities your app needs. LightShell configures these in lightshell.json:
{ "build": { "mac": { "identity": "Developer ID Application: Your Name (TEAMID)", "entitlements": { "com.apple.security.network.client": true, "com.apple.security.files.user-selected.read-write": true } } }}Common entitlements for LightShell apps:
| Entitlement | Purpose |
|---|---|
com.apple.security.network.client | Make outbound network requests (needed for lightshell.http.fetch) |
com.apple.security.files.user-selected.read-write | Read/write files selected via open/save dialogs |
com.apple.security.files.downloads.read-write | Access the Downloads folder |
com.apple.security.automation.apple-events | Send Apple Events to other apps |
LightShell generates an entitlements.plist from this configuration and passes it to codesign during signing. If you do not specify entitlements, LightShell uses sensible defaults (network.client and files.user-selected.read-write).
Notarization
Section titled “Notarization”Notarization submits your signed app to Apple for automated security scanning. Once approved, Apple records that your app is safe, and macOS will open it without any warnings on any Mac.
Notarization requires your Apple ID and Team ID. Add them to your build configuration or pass them as environment variables:
{ "build": { "mac": { "identity": "Developer ID Application: Your Name (TEAMID)", "appleId": "your@email.com", "teamId": "TEAMID" } }}You also need an app-specific password for your Apple ID. Generate one at appleid.apple.com under Security > App-Specific Passwords. Store it in your Keychain:
xcrun notarytool store-credentials "lightshell-notarize" \ --apple-id "your@email.com" \ --team-id "TEAMID" \ --password "xxxx-xxxx-xxxx-xxxx"Running Notarization
Section titled “Running Notarization”lightshell build --target dmg --sign --notarizeThis performs the following steps:
- Builds and signs the
.appbundle - Packages it into a DMG
- Submits the DMG to Apple’s notarization service via
xcrun notarytool submit - Waits for Apple to complete the scan (typically 1-5 minutes)
- Staples the notarization ticket to the DMG via
xcrun stapler staple
Output:
✓ Built my-app in 1.2s → 5.1MB✓ Signed MyApp.app✓ Packaged DMG → dist/MyApp-1.0.0.dmg✓ Submitted for notarization... waiting✓ Notarized successfully (ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)✓ Stapled notarization ticket to MyApp-1.0.0.dmgVerifying a Signed App
Section titled “Verifying a Signed App”Check the signature and notarization status:
# Verify the code signaturecodesign --verify --deep --strict --verbose=2 dist/MyApp.app
# Check notarization statusspctl --assess --verbose=2 dist/MyApp.app
# Check a DMGspctl --assess --type open --verbose=2 dist/MyApp-1.0.0.dmgA properly signed and notarized app shows:
dist/MyApp.app: acceptedsource=Notarized Developer IDTroubleshooting
Section titled “Troubleshooting””No identity found”
Section titled “”No identity found””Your signing certificate is not in the Keychain. Verify with:
security find-identity -v -p codesigningIf empty, download your certificate from the Apple Developer portal and install it.
”The signature is invalid”
Section titled “”The signature is invalid””The app was modified after signing. Rebuild and sign again. Ensure no post-processing step modifies the .app bundle after signing.
Notarization rejected
Section titled “Notarization rejected”Check the detailed log:
xcrun notarytool log {submission-id} --keychain-profile "lightshell-notarize"Common reasons for rejection:
- Missing hardened runtime (
--options runtime) — LightShell includes this automatically - Unsigned nested binaries — LightShell uses
--deepto sign everything - Missing entitlements for capabilities the app uses
”errSecInternalComponent”
Section titled “”errSecInternalComponent””This usually means the Keychain is locked. Unlock it:
security unlock-keychain ~/Library/Keychains/login.keychain-dbIn CI environments, you may need to create and unlock a temporary Keychain.
CI Integration
Section titled “CI Integration”For automated signing in GitHub Actions:
jobs: build-signed: runs-on: macos-latest steps: - uses: actions/checkout@v4 - name: Import certificate env: CERTIFICATE_P12: ${{ secrets.CERTIFICATE_P12 }} CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }} run: | echo "$CERTIFICATE_P12" | base64 --decode > certificate.p12 security create-keychain -p "" build.keychain security import certificate.p12 -k build.keychain -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain security default-keychain -s build.keychain - name: Build and sign run: lightshell build --target dmg --signStore your .p12 certificate file as a base64-encoded GitHub Secret.