Agent skill
macos-app-bundle
macOS application bundling with cargo-bundle and entitlements
Install this agent skill to your Project
npx add-skill https://github.com/johnlindquist/script-kit-next/tree/main/.opencode/skill/macos-app-bundle
SKILL.md
macos-app-bundle
Create distributable macOS .app bundles for Rust applications using cargo-bundle, with proper code signing, entitlements, and notarization for Gatekeeper approval.
Quick Reference
# Install cargo-bundle (Zed's fork)
cargo install cargo-bundle --git https://github.com/zed-industries/cargo-bundle.git --branch zed-deploy
# Build and bundle
cargo bundle --release
# Sign and notarize
codesign --deep --force --timestamp --options runtime \
--entitlements entitlements.plist \
--sign "Developer ID Application: Name (TEAM_ID)" \
"target/release/bundle/osx/App.app"
xcrun notarytool submit App.dmg --apple-id "$APPLE_ID" --password "$APPLE_PASSWORD" --team-id "$TEAM_ID" --wait
xcrun stapler staple App.dmg
cargo-bundle
Use Zed's fork for GPUI apps - it's battle-tested and actively maintained.
Installation
cargo install cargo-bundle --git https://github.com/zed-industries/cargo-bundle.git --branch zed-deploy
Usage
# Development build
cargo bundle
# Release build
cargo bundle --release
# Specific target (universal binary)
cargo bundle --release --target aarch64-apple-darwin
cargo bundle --release --target x86_64-apple-darwin
Output: target/release/bundle/osx/App Name.app
Bundle Metadata
Configure in Cargo.toml under [package.metadata.bundle]:
[package.metadata.bundle]
name = "Script Kit" # Display name (can include spaces)
identifier = "com.scriptkit.app" # Reverse-DNS bundle ID
icon = ["assets/icon.png", "assets/icon@2x.png", "assets/icon.icns"]
version = "0.1.0" # Must match package version
copyright = "Copyright (c) 2024 Script Kit. All rights reserved."
category = "public.app-category.developer-tools"
short_description = "Automation made simple"
osx_minimum_system_version = "10.15" # Catalina minimum
osx_url_schemes = ["scriptkit"] # Custom URL handler
resources = ["assets/*"] # Bundle these files
osx_info_plist_exts = ["assets/Info.plist.ext"] # Custom Info.plist additions
Common Categories
| Category | Use Case |
|---|---|
public.app-category.developer-tools |
IDEs, terminals, automation |
public.app-category.productivity |
Task managers, note-taking |
public.app-category.utilities |
System tools, menu bar apps |
public.app-category.business |
Enterprise applications |
Info.plist
cargo-bundle generates Contents/Info.plist from metadata. Extend it with osx_info_plist_exts:
assets/Info.plist.ext
<key>LSUIElement</key>
<true/>
<key>LSBackgroundOnly</key>
<false/>
Key Info.plist Fields
| Key | Description | Example |
|---|---|---|
CFBundleIdentifier |
Unique app ID | com.scriptkit.app |
CFBundleName |
Display name | Script Kit |
CFBundleVersion |
Build number | 1 |
CFBundleShortVersionString |
Version string | 1.0.0 |
LSMinimumSystemVersion |
Minimum macOS | 10.15 |
CFBundleURLTypes |
URL schemes | See below |
LSUIElement |
Agent app (no Dock) | true/false |
LSBackgroundOnly |
Background-only | true/false |
URL Scheme Registration
Registered via osx_url_schemes in Cargo.toml. Results in:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.scriptkit.app</string>
<key>CFBundleURLSchemes</key>
<array>
<string>scriptkit</string>
</array>
</dict>
</array>
Handle in Rust via gpui::AppContext::on_open_urls().
LSUIElement
Makes app an agent app - runs without Dock icon or menu bar. Perfect for:
- Launcher apps (like Raycast, Alfred)
- Menu bar utilities
- System tray applications
- Background daemons with UI
<!-- assets/Info.plist.ext -->
<key>LSUIElement</key>
<true/>
LSUIElement vs LSBackgroundOnly
| Property | Dock Icon | Menu Bar | Windows |
|---|---|---|---|
| Neither set | Yes | Yes | Yes |
LSUIElement=true |
No | No | Yes |
LSBackgroundOnly=true |
No | No | No |
Script Kit uses LSUIElement=true with LSBackgroundOnly=false - no Dock/menu but can show windows.
Entitlements
Entitlements enable specific capabilities and are required for notarization.
entitlements.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>
<!-- HARDENED RUNTIME EXCEPTIONS -->
<!-- Required for JIT (bun/Node.js) -->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<!-- Required for generated code execution -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<!-- Load non-Apple signed libraries -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<!-- Child process spawning -->
<key>com.apple.security.cs.disable-executable-page-protection</key>
<true/>
<!-- AUTOMATION -->
<key>com.apple.security.automation.apple-events</key>
<true/>
<!-- HARDWARE -->
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<!-- NETWORK -->
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<!-- FILE ACCESS -->
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key>
<true/>
</dict>
</plist>
Entitlements vs Runtime Permissions
| Type | When Applied | User Interaction |
|---|---|---|
| Entitlements | Baked into code signature | None - enables capability |
| Runtime Permissions | First use | User grants in System Preferences |
Example: Accessibility requires BOTH:
- No special entitlement needed
- User grants permission in System Preferences > Security & Privacy > Accessibility
Common Entitlements
| Entitlement | Purpose |
|---|---|
com.apple.security.cs.allow-jit |
JavaScript/JIT compilation |
com.apple.security.cs.allow-unsigned-executable-memory |
Runtime code generation |
com.apple.security.cs.disable-library-validation |
Load third-party dylibs |
com.apple.security.automation.apple-events |
AppleScript/osascript |
com.apple.security.network.client |
Outbound network |
com.apple.security.network.server |
Listen on ports |
com.apple.security.device.camera |
Camera access |
com.apple.security.device.audio-input |
Microphone access |
URL Schemes
Register custom URL handlers to open your app via yourapp:// links.
Configuration
# Cargo.toml
[package.metadata.bundle]
osx_url_schemes = ["scriptkit"]
Handling in Code
// In your GPUI app
cx.on_open_urls(|urls, cx| {
for url in urls {
if url.scheme() == "scriptkit" {
// Handle: scriptkit://action?param=value
handle_url(url, cx);
}
}
});
Testing
open "scriptkit://run?script=hello"
Code Signing
Prerequisites
- Apple Developer Account ($99/year)
- Developer ID Application certificate (for distribution outside App Store)
Find Your Identity
security find-identity -v -p codesigning
Sign the Bundle
# Sign with hardened runtime (required for notarization)
codesign --deep --force --timestamp --options runtime \
--entitlements entitlements.plist \
--sign "Developer ID Application: Your Name (TEAM_ID)" \
"target/release/bundle/osx/Script Kit.app"
# Verify
codesign -vvv --deep --strict "Script Kit.app"
spctl -a -vvv "Script Kit.app"
Ad-Hoc Signing (Development Only)
codesign --force --deep --sign - "Script Kit.app"
Warning: Ad-hoc signed apps trigger Gatekeeper warnings.
Environment Variables for CI
export APPLE_SIGNING_IDENTITY="Developer ID Application: Your Name (TEAM_ID)"
export APPLE_ID="your@email.com"
export APPLE_APP_PASSWORD="xxxx-xxxx-xxxx-xxxx" # App-specific password
export APPLE_TEAM_ID="XXXXXXXXXX"
Notarization
Apple requires notarization for apps distributed outside the App Store (macOS 10.15+).
Submit for Notarization
# Create DMG first
hdiutil create -volname "Script Kit" \
-srcfolder "target/release/bundle/osx/Script Kit.app" \
-ov -format UDZO \
"ScriptKit.dmg"
# Sign DMG
codesign --force --sign "$APPLE_SIGNING_IDENTITY" "ScriptKit.dmg"
# Submit and wait
xcrun notarytool submit "ScriptKit.dmg" \
--apple-id "$APPLE_ID" \
--password "$APPLE_APP_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait
# Staple ticket to DMG
xcrun stapler staple "ScriptKit.dmg"
Check Notarization Status
xcrun notarytool history --apple-id "$APPLE_ID" --password "$APPLE_APP_PASSWORD" --team-id "$APPLE_TEAM_ID"
Common Notarization Errors
| Error | Solution |
|---|---|
| "Invalid credentials" | Check APPLE_ID and app-specific password |
| "The signature is invalid" | Ensure entitlements.plist exists and is valid |
| "Hardened runtime not enabled" | Add --options runtime to codesign |
| "Contains unsigned code" | Sign all nested binaries with --deep |
Icon Generation
Required Sizes for .icns
| Size | Filename |
|---|---|
| 16x16 | icon_16x16.png |
| 32x32 | icon_16x16@2x.png, icon_32x32.png |
| 64x64 | icon_32x32@2x.png |
| 128x128 | icon_128x128.png |
| 256x256 | icon_128x128@2x.png, icon_256x256.png |
| 512x512 | icon_256x256@2x.png, icon_512x512.png |
| 1024x1024 | icon_512x512@2x.png |
Generate from 1024x1024 Source
mkdir MyIcon.iconset
sips -z 16 16 icon-1024.png --out MyIcon.iconset/icon_16x16.png
sips -z 32 32 icon-1024.png --out MyIcon.iconset/icon_16x16@2x.png
sips -z 32 32 icon-1024.png --out MyIcon.iconset/icon_32x32.png
sips -z 64 64 icon-1024.png --out MyIcon.iconset/icon_32x32@2x.png
sips -z 128 128 icon-1024.png --out MyIcon.iconset/icon_128x128.png
sips -z 256 256 icon-1024.png --out MyIcon.iconset/icon_128x128@2x.png
sips -z 256 256 icon-1024.png --out MyIcon.iconset/icon_256x256.png
sips -z 512 512 icon-1024.png --out MyIcon.iconset/icon_256x256@2x.png
sips -z 512 512 icon-1024.png --out MyIcon.iconset/icon_512x512.png
cp icon-1024.png MyIcon.iconset/icon_512x512@2x.png
iconutil -c icns MyIcon.iconset
Bundle Structure
Script Kit.app/
├── Contents/
│ ├── Info.plist # App metadata (generated + extensions)
│ ├── MacOS/
│ │ └── script-kit-gpui # Main executable
│ ├── Resources/
│ │ ├── AppIcon.icns # App icon
│ │ ├── assets/ # Bundled resources
│ │ └── ...
│ ├── Frameworks/ # Bundled frameworks (if any)
│ └── _CodeSignature/ # Code signature
Anti-patterns
Don't: Skip Hardened Runtime
# WRONG - won't notarize
codesign --force --sign "$IDENTITY" App.app
# CORRECT
codesign --force --options runtime --sign "$IDENTITY" App.app
Don't: Forget Entitlements
# WRONG - JIT apps will crash
codesign --options runtime --sign "$IDENTITY" App.app
# CORRECT
codesign --options runtime --entitlements entitlements.plist --sign "$IDENTITY" App.app
Don't: Sign Before Building
# WRONG - signature invalidated
codesign ... App.app
cargo bundle --release # Overwrites!
# CORRECT
cargo bundle --release
codesign ... App.app
Don't: Use Overly Broad Entitlements
<!-- WRONG - App Store rejection -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.get-task-allow</key>
<true/>
Only include entitlements you actually need.
Don't: Hardcode Signing Identity
# WRONG - breaks on other machines
codesign --sign "Developer ID Application: John (ABC123)" App.app
# CORRECT - use environment variable
codesign --sign "$APPLE_SIGNING_IDENTITY" App.app
Don't: Forget to Staple
# WRONG - users still see Gatekeeper warning
xcrun notarytool submit App.dmg --wait
# Done!
# CORRECT - staple the ticket
xcrun notarytool submit App.dmg --wait
xcrun stapler staple App.dmg
CI/CD Integration
GitHub Actions Secrets
| Secret | Description |
|---|---|
APPLE_CERTIFICATE_BASE64 |
Base64-encoded .p12 certificate |
APPLE_CERTIFICATE_PASSWORD |
Password for .p12 |
APPLE_ID |
Apple Developer email |
APPLE_APP_PASSWORD |
App-specific password |
APPLE_TEAM_ID |
10-character Team ID |
Workflow Steps
- Build release binary
- Create .app bundle with cargo-bundle
- Import certificate to temporary keychain
- Sign with hardened runtime + entitlements
- Create DMG
- Notarize and staple
- Upload to release
See BUNDLING.md for full GitHub Actions workflow example.
References
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
Generate Component Documentation
Based on existing docs styles and specific API implementations, and referencing same name stories, generate comprehensive documentation for the new component.
Generate Component Story
Generate a comprehensive story for a new component for as example.
new-component
How to write a new component of GPUI Component.
troubleshooting
Diagnose and fix common Script Kit issues. Use when the user reports bugs, crashes, missing features, or unexpected behavior in Script Kit GPUI.
script-authoring
Create and manage TypeScript scripts for Script Kit. Use when the user wants to write a new script, edit an existing script, or understand Script Kit's SDK and metadata system.
agents
Create mdflow-backed agent files for Script Kit. Use when the user wants to create AI agents, configure agent backends (Claude, Gemini, Codex), or manage agent metadata.
Didn't find tool you were looking for?