macOS Apps with AppKit skill for Codex
Real Mac apps: menus, sandbox, notarized. Codex picks the distribution channel first because it decides which APIs are allowed, then builds with SwiftUI scenes, menu commands and Settings, dropping to AppKit only where SwiftUI falls short. It keeps sandbox entitlements minimal, persists file access with security-scoped bookmarks, registers login items only behind a user toggle, and signs, notarizes and staples Developer ID builds.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- macos-appkit Installs to ~/.agents/skills/macos-appkit/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install macos-appkit The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project macos-appkit Installs to .agents/skills/macos-appkit/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/macos-appkit.zipunzip -o macos-appkit.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) macos-appkit PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $macos-appkit, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds native macOS apps with SwiftUI and AppKit - App and Scene structure, MenuBarExtra menu bar utilities, Settings windows, NSWindow and NSViewRepresentable interop, menus and keyboard shortcuts, App Sandbox entitlements and security-scoped bookmarks, launch at login with SMAppService, hardened runtime, Developer ID signing and notarytool. Use when creating a Mac app or menu bar app, bridging SwiftUI and AppKit, fixing sandbox file-access or entitlement errors, or preparing a Mac build for notarization or the Mac App Store.
Use it when
- Creating a Mac app or menu bar utility in Swift
- Bridging SwiftUI and AppKit views, windows or app delegate hooks
- Sandbox, entitlement or file-access errors on macOS
- Preparing a Developer ID or Mac App Store build
Use something else when
- The views target iPhone and iPad layouts: use swiftui-views
- You need DMG installers, Sparkle feeds or cross-platform release pipelines: use desktop-app-distribution
How to check the result
codesign --verify --deep --strict and spctl -a -t exec both accept the exported app, sandboxed file access survives a relaunch, and every command is reachable from the menu bar.
The mistake it prevents
Shipping a helper tool or embedded framework without hardened runtime signing, which only surfaces as a notarization rejection at release time.
Done well means: the app behaves like a Mac app (menus, shortcuts, multiple windows, Settings, resizable layouts), uses SwiftUI where it fits and AppKit where SwiftUI falls short, runs sandboxed with the fewest entitlements that work, and every distributed build is signed, notarized and stapled.
Before changing anything
- Read
AGENTS.md, then the Xcode project orPackage.swift: deployment target, targets (app, helpers, extensions),Info.plistand.entitlementsfiles. - Decide the distribution channel now; it constrains APIs:
| Channel | Requires | Restricts |
|---|---|---|
| Mac App Store | App Sandbox, App Store review | No Accessibility API control of other apps, no arbitrary file access, no self-updating |
| Developer ID (direct download) | Hardened runtime, notarization | Sandbox optional; you ship your own updater (Sparkle) |
| Internal only | Development or Developer ID signing | Gatekeeper still blocks unsigned downloads |
- Build from the command line to see errors without Xcode UI:
xcodebuild -scheme MyApp -configuration Debug -destination 'platform=macOS' build.
App structure
@main
struct NotesApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
@State private var store = NoteStore()
var body: some Scene {
WindowGroup { ContentView().environment(store) }
.commands {
CommandGroup(after: .newItem) {
Button("New Folder") { store.newFolder() }
.keyboardShortcut("n", modifiers: [.command, .shift])
}
}
Window("Activity", id: "activity") { ActivityView() }
Settings { SettingsView() } // Cmd-, opens it automatically
}
}
- Use
WindowGroupfor document-like windows,Windowfor singletons,Settingsfor preferences. Open windows with@Environment(\.openWindow); open Settings from code withSettingsLinkor@Environment(\.openSettings)(macOS 14+). - Put application lifecycle hooks (open URLs, dock menu, termination) in an
NSApplicationDelegatevia the adaptor. - Every action reachable by mouse should be in the menu bar with a shortcut; macOS users and accessibility tools rely on menus.
Menu bar apps
@main
struct ClipApp: App {
var body: some Scene {
MenuBarExtra("Clip", systemImage: "paperclip") {
ClipPanel()
}
.menuBarExtraStyle(.window) // .menu for a plain menu
Settings { SettingsView() }
}
}
- Set
LSUIElementtoYESin Info.plist to hide the Dock icon and app menu. - Provide a Quit item; with no Dock icon there is no other way to quit.
- Launch at login with
SMAppService.mainApp.register()(macOS 13+) behind a user toggle, and readSMAppService.mainApp.statusto show the real state. Do not register silently. - For a global hotkey, use a library built on Carbon
RegisterEventHotKeyor the system’s shortcut APIs;NSEvent.addGlobalMonitorForEventsfor key events needs Accessibility permission and is not allowed for Mac App Store apps that monitor other apps.
When to drop to AppKit
| Need | Approach |
|---|---|
| Large tables with sorting, column resizing | SwiftUI Table first; NSTableView via NSViewRepresentable if performance or features fall short |
| Rich text editing | NSTextView wrapped in NSViewRepresentable (SwiftUI TextEditor is limited) |
| Window chrome (titlebar accessories, toolbar styles, floating panels) | NSWindow / NSPanel access through the delegate or a window accessor view |
| Drag and drop of files | SwiftUI .dropDestination(for: URL.self) first |
| Services, Dock menu, custom about panel | NSApplicationDelegate |
struct RichTextView: NSViewRepresentable {
@Binding var text: NSAttributedString
func makeNSView(context: Context) -> NSScrollView {
let scroll = NSTextView.scrollableTextView()
(scroll.documentView as? NSTextView)?.delegate = context.coordinator
return scroll
}
func updateNSView(_ view: NSScrollView, context: Context) {
guard let tv = view.documentView as? NSTextView, tv.attributedString() != text else { return }
tv.textStorage?.setAttributedString(text)
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
final class Coordinator: NSObject, NSTextViewDelegate {
let parent: RichTextView
init(_ p: RichTextView) { parent = p }
func textDidChange(_ n: Notification) {
if let tv = n.object as? NSTextView { parent.text = tv.attributedString() }
}
}
}
Sandbox and file access
Common entitlements (.entitlements):
| Key | Grants |
|---|---|
com.apple.security.app-sandbox | Turns the sandbox on |
com.apple.security.files.user-selected.read-write | Files the user picks in open/save panels or drags in |
com.apple.security.network.client | Outgoing connections |
com.apple.security.network.server | Listening sockets |
com.apple.security.device.camera / .audio-input | Camera, microphone (plus usage strings in Info.plist) |
To keep access to a user-chosen file or folder across launches, store a security-scoped bookmark:
let data = try url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
// later
var stale = false
let restored = try URL(resolvingBookmarkData: data, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &stale)
guard restored.startAccessingSecurityScopedResource() else { throw AccessError.denied }
defer { restored.stopAccessingSecurityScopedResource() }
if stale { /* re-create and save the bookmark */ }
- Sandboxed apps see their container as home (
~/Library/Containers/<bundle-id>/Data); hard-coded~/Documentspaths point inside the container. - Ask for the narrowest entitlement; every extra one needs justification in App Review.
- Temporary exception entitlements are rarely approved; redesign around user selection.
Signing and notarization (Developer ID)
- Enable Hardened Runtime on every target. Add runtime exceptions
(
com.apple.security.cs.allow-jit,disable-library-validation) only when a specific dependency requires it, and note why. - Archive and export with method
developer-id, then notarize and staple:
xcodebuild -scheme MyApp -configuration Release -archivePath build/MyApp.xcarchive archive
xcodebuild -exportArchive -archivePath build/MyApp.xcarchive -exportOptionsPlist ExportOptions.plist -exportPath build/export
ditto -c -k --keepParent build/export/MyApp.app build/MyApp.zip
xcrun notarytool submit build/MyApp.zip --keychain-profile notary --wait # needs network
xcrun stapler staple build/export/MyApp.app
spctl -a -vvv -t exec build/export/MyApp.app # expect: accepted, Notarized Developer ID
notarytool store-credentials notary saves credentials once in the keychain; never put
an app-specific password in a script. For DMG/pkg packaging, Sparkle updates and CI
pipelines, follow desktop-app-distribution.
Gotchas
Info.plistusage descriptions (NSCameraUsageDescription, etc.) are mandatory; the app is terminated on first access without them.- Helper tools, login items and embedded frameworks must be signed with the same team
and hardened runtime, or notarization fails with an unsigned-binary error. Read the
notarization log:
xcrun notarytool log <submission-id> --keychain-profile notary. - SwiftUI on macOS defaults to sizes that look wrong on small screens; set
.frame(minWidth:minHeight:)and.defaultSizeper window. - Test in both light and dark appearance and with Increase Contrast; use semantic colors
(
Color(nsColor: .windowBackgroundColor)), not fixed values. - Older deployment targets lack newer SwiftUI scene APIs; guard with
if #available(macOS 14, *).
Verification
- Clean Release build with no warnings about entitlements or signing.
- Sandboxed build: open, save, reopen after relaunch through bookmarks; network calls work; denied paths fail with a readable message.
codesign --verify --deep --strict --verbose=2 MyApp.appandspctlboth pass on the exported app; launch it on a second Mac after downloading through a browser.- Keyboard-only walkthrough: every command reachable from the menu bar.
Output
**Channel:** App Store | Developer ID | internal
**Scenes:** <windows, Settings, MenuBarExtra>
**AppKit bridges:** <component -> reason>
**Entitlements:** <key -> feature that needs it>
**Notarization:** submission id, status, stapled yes/no What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "macOS Apps with AppKit"
short_description: "Real Mac apps: menus, sandbox, notarized"
default_prompt: "$macos-appkit Build a menu bar app in SwiftUI that lives without a Dock icon and launches at login" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.