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.

  • Mobile & Desktop
  • $macos-appkit
  • 195 lines
  • 2 files
  • ~2,200 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- macos-appkit

Installs to ~/.agents/skills/macos-appkit/ for every project on this machine.

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

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.

~/.agents/skills/macos-appkit/SKILL.md

--- name: macos-appkit description: Builds native macOS apps with SwiftUI and AppKit - App and Scene structure, MenuBarExtra menu bar utilities, Settings windows, NSWindow and… license: MIT ---

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

  1. Read AGENTS.md, then the Xcode project or Package.swift: deployment target, targets (app, helpers, extensions), Info.plist and .entitlements files.
  2. Decide the distribution channel now; it constrains APIs:
ChannelRequiresRestricts
Mac App StoreApp Sandbox, App Store reviewNo Accessibility API control of other apps, no arbitrary file access, no self-updating
Developer ID (direct download)Hardened runtime, notarizationSandbox optional; you ship your own updater (Sparkle)
Internal onlyDevelopment or Developer ID signingGatekeeper still blocks unsigned downloads
  1. 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 WindowGroup for document-like windows, Window for singletons, Settings for preferences. Open windows with @Environment(\.openWindow); open Settings from code with SettingsLink or @Environment(\.openSettings) (macOS 14+).
  • Put application lifecycle hooks (open URLs, dock menu, termination) in an NSApplicationDelegate via the adaptor.
  • Every action reachable by mouse should be in the menu bar with a shortcut; macOS users and accessibility tools rely on menus.
@main
struct ClipApp: App {
    var body: some Scene {
        MenuBarExtra("Clip", systemImage: "paperclip") {
            ClipPanel()
        }
        .menuBarExtraStyle(.window)      // .menu for a plain menu
        Settings { SettingsView() }
    }
}
  • Set LSUIElement to YES in 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 read SMAppService.mainApp.status to show the real state. Do not register silently.
  • For a global hotkey, use a library built on Carbon RegisterEventHotKey or the system’s shortcut APIs; NSEvent.addGlobalMonitorForEvents for key events needs Accessibility permission and is not allowed for Mac App Store apps that monitor other apps.

When to drop to AppKit

NeedApproach
Large tables with sorting, column resizingSwiftUI Table first; NSTableView via NSViewRepresentable if performance or features fall short
Rich text editingNSTextView 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 filesSwiftUI .dropDestination(for: URL.self) first
Services, Dock menu, custom about panelNSApplicationDelegate
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):

KeyGrants
com.apple.security.app-sandboxTurns the sandbox on
com.apple.security.files.user-selected.read-writeFiles the user picks in open/save panels or drags in
com.apple.security.network.clientOutgoing connections
com.apple.security.network.serverListening sockets
com.apple.security.device.camera / .audio-inputCamera, 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 ~/Documents paths 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.plist usage 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 .defaultSize per 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

  1. Clean Release build with no warnings about entitlements or signing.
  2. Sandboxed build: open, save, reopen after relaunch through bookmarks; network calls work; denied paths fail with a readable message.
  3. codesign --verify --deep --strict --verbose=2 MyApp.app and spctl both pass on the exported app; launch it on a second Mac after downloading through a browser.
  4. 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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem