← Blog

Sandboxing a Mac system utility: four APIs, four trade-offs

April 30, 2026 · Juri Du

ShowMode is a small Mac menubar app I am launching on May 13. It does one thing: a single keyboard shortcut hides desktop icons, silences notifications, mutes system audio, and minimizes a configurable list of apps. Press it again and everything reverses, exactly as it was.

The user-facing pitch is one sentence. The engineering reality is that none of those four operations have a single clean, sandbox-friendly API. Building ShowMode meant picking a different mechanism for each one and accepting different trade-offs each time. This post is a tour of the four mechanisms I shipped, why I picked each one, and the things I am still nervous about heading into App Review.

Target context: macOS 13 (Ventura) and up. Native Swift / AppKit. Two distribution targets — Mac App Store with sandbox on, and direct distribution via Lemon Squeezy with sandbox off. The codebase is the same in both cases; the entitlements diverge.

1. Minimizing windows: AXUIElement, not AppleScript

The first version of ShowMode used AppleScript to minimize windows:

tell application "Slack"
    set miniaturized of every window to true
end tell

This works, but it has three problems. First, every targeted app needs Automation permission granted separately by the user in System Settings — that is an N-app prompt fan-out that ruins onboarding. Second, AppleScript dispatch goes through the scripting engine, which adds noticeable latency when you are touching ten apps at once. Third, many apps either lack a usable scripting dictionary or expose miniaturized in inconsistent ways.

The version I shipped uses the Accessibility API directly:

let apps = NSRunningApplication.runningApplications(withBundleIdentifier: bundleID)

for app in apps {
    let axApp = AXUIElementCreateApplication(app.processIdentifier)

    var windowsRef: CFTypeRef?
    let result = AXUIElementCopyAttributeValue(
        axApp, kAXWindowsAttribute as CFString, &windowsRef
    )

    guard result == .success,
          let windows = windowsRef as? [AXUIElement] else { continue }

    for window in windows {
        let val = true as CFTypeRef
        AXUIElementSetAttributeValue(window, kAXMinimizedAttribute as CFString, val)
    }
}

This is one Accessibility prompt for the whole app, not one per target. It is faster. And it works against any standard AppKit window without depending on each app's scripting dictionary.

The trade-off: Electron apps tend to ignore kAXMinimizedAttribute>. Slack, Discord, and the Microsoft Teams Electron build all silently no-op the set. AXUIElementSetAttributeValue returns .success regardless, so you cannot detect the failure from the return code. ShowMode handles this with a coarse fallback: if a window's minimize state does not actually change after the set, the next attempt for that bundle ID falls back to NSRunningApplication.hide(), which works on every app because it goes through AppKit's own hide pipeline rather than asking the app to cooperate.

hide() is coarser — it hides the entire app, not selected windows — but for a "make my screen presentable" feature, that is the correct semantics anyway. You wanted Slack out of the way, not Slack's "Threads" window specifically.

The other thing worth noting: I save the prior minimize state of every window before touching it, so that on restore I only un-minimize windows that were not already minimized. If you had three Safari windows open and one of them was already in the Dock, that one stays in the Dock when ShowMode restores.

2. Audio mute: CoreAudio, master channel, fall back to per-channel

Muting system audio is the operation with the cleanest API and the messiest reality. Cleanest: kAudioDevicePropertyMute on the default output device. One property, one set:

var address = AudioObjectPropertyAddress(
    mSelector: kAudioDevicePropertyMute,
    mScope:    kAudioDevicePropertyScopeOutput,
    mElement:  kAudioObjectPropertyElementMain   // master channel
)

var muteValue: UInt32 = 1
AudioObjectSetPropertyData(deviceID, &address, 0, nil,
                           UInt32(MemoryLayout<UInt32>.size), &muteValue)

Messiest: not every output device exposes a settable mute property on the master channel. Built-in MacBook speakers do. AirPods do. But various HDMI outputs and a non-trivial set of USB DACs either do not implement kAudioDevicePropertyMute at all, or implement it on individual channels (left and right, elements 1 and 2) but not on the master.

The shipped logic checks AudioObjectIsPropertySettable on the master first. If the master is not settable, it falls back to setting mute on channels 1 and 2 individually. If the device does not implement mute on any channel — which is rare but real on cheap HDMI passthrough — the Preferences pane shows a one-line warning telling the user that their current output device does not support programmatic mute, and that ShowMode will skip that step on this hardware. Honest is better than mysterious.

One detail I did not expect to matter: I also save the prior mute state. If the user already had their machine muted before activating ShowMode, restore should leave them muted, not unmute. This sounds obvious. The original code had a bug where I was force-unmuting on restore and creating a small but real "ShowMode unmuted my computer in a meeting" failure mode. Always save state, always restore exactly what was there.

Sandbox status: CoreAudio property reads and writes work fine in the sandbox without any entitlement. It is hardware control, not file or network access, so the sandbox does not gate it.

3. Desktop icons: defaults write com.apple.finder CreateDesktop + the sandbox tax

This is the single feature where the sandbox actually hurts.

The mechanism is well-known to Mac power users: write CreateDesktop = false to com.apple.finder and restart Finder. The icons disappear; toggle it back and they return. There is no public framework API for this — Finder owns desktop rendering and exposes no programmatic toggle. defaults + killall Finder is the supported route, full stop.

In a non-sandboxed app you can run that directly via Process:

// Direct distribution (Lemon Squeezy build) — sandbox off
let task = Process()
task.launchPath = "/bin/sh"
task.arguments = ["-c",
    "defaults write com.apple.finder CreateDesktop -bool false && killall Finder"
]
try? task.run()

In the sandboxed Mac App Store build, Process cannot launch arbitrary executables. The sandboxed path uses NSAppleScript with do shell script:

let script = """
do shell script "defaults write com.apple.finder CreateDesktop false && killall Finder"
"""
NSAppleScript(source: script)?.executeAndReturnError(nil)

That works, but it requires two things in the bundle. In Info.plist:

NSAppleEventsUsageDescription = "ShowMode toggles desktop icon visibility,
which requires restarting Finder."

And in .entitlements:

com.apple.security.temporary-exception.apple-events = [
    "com.apple.finder",
    "com.apple.dock"
]

That second one — com.apple.security.temporary-exception.apple-events — is the part I am genuinely nervous about for App Review. Temporary-exception entitlements are explicitly discretionary, scoped to specific bundle identifiers, and require justification in App Store Connect review notes. Apple has been known to reject them. My justification: "ShowMode toggles desktop icon visibility by restarting Finder. There is no other supported API for hiding desktop icons. The exception is scoped only to com.apple.finder for the icons feature and com.apple.dock for the Dock auto-hide feature. No other Apple Events targets are reachable."

If review rejects, the fallback is to ship the icons feature only in the direct version and have the MAS version omit it. I would rather not — feature parity matters — but I have a build flag ready for that case.

One UX cost worth flagging: killall Finder restarts Finder. On macOS 14 Sonoma the restart takes about 1.5 seconds. During that 1.5 seconds, any open Finder windows close. The first version of ShowMode toggled silently and users would press the shortcut, see Finder windows vanish, and assume the app had crashed something. The shipped version shows a tiny overlay that says "Adjusting desktop…" for 1.5 seconds whenever the icons toggle runs. The honesty resolves the WTF.

4. Notifications: macOS Focus, three OS versions, three behaviors

This is the feature where I lost the most time to OS-version drift.

macOS Ventura (13), Sonoma (14), and Sequoia (15) all expose Focus modes, but the supported way to programmatically toggle Do Not Disturb has shifted in each one. There is no stable public API in any of them — UNUserNotificationCenter manages your app's notifications, not the system Focus state. The Focus state is owned by the focusd daemon and the supported user-facing surface is the Control Center toggle plus Shortcuts.

What works in all three versions, and what ShowMode actually ships, is the Shortcuts approach: ShowMode triggers a system Shortcut on activation that turns Do Not Disturb on, and another on deactivation that turns it off. The user installs two small shortcuts on first launch (ShowMode prompts and links them directly). This is awkward — it requires a one-time setup step — but it is the only path that survives OS version drift, and it is the path Apple's own documentation now points to for "control Focus from another app."

I tried two other paths first. Setting com.apple.donotdisturb defaults keys works on macOS 13 but not 14+. Sending the appropriate AppleScript to System Events works on 14 but not 15. The Shortcuts route is uglier in onboarding but is the only one that does not require version-specific code paths I would then have to maintain.

If a future macOS exposes a real public API for Focus state, ShowMode will switch to it the same week.

What I would tell another solo Mac dev

If you are building a system utility that crosses sandbox boundaries, three things saved me real time:

  1. Pick the API based on the failure mode you can detect. AXUIElement returns success on apps that ignore the set; CoreAudio is honest about whether mute is settable. Where you can detect failure cleanly, prefer that API even if it is more code.
  2. Always save and restore prior state, not absolute state. If the user had Slack hidden, leave it hidden. If they were muted, leave them muted. The temptation to write "set everything to my preferred state" looks the same on the surface and is wrong almost every time.
  3. Maintain two builds from the same source: sandboxed and not. The sandbox forces architectural discipline; the non-sandbox build lets you sidestep entitlement-exception risk for direct distribution. Same Swift code, different .entitlements, conditional Process vs NSAppleScript at one well-marked seam. Both targets shipped from the same source tree.

ShowMode is on the Mac App Store and direct from getshowmode.com on May 13. $4.99 one-time on MAS, $6.99 direct. Native Swift, no Electron, zero network, no telemetry. If you want to be notified at launch, the email list is on the homepage.

If you have shipped a similar utility and have war stories about temporary-exception entitlements surviving (or not surviving) App Review, I would genuinely like to hear them. @juribuilds on X.