App Sandbox silently blocks Accessibility permission

This post is also available in Korean.

My menu bar app never showed up in System Settings → Accessibility. No error, no prompt, no entry to toggle. Here is what the TCC daemon logs actually say, and the one-variable test that pinned it down.

I was building a small menu bar utility that reads the text caret position of whatever app you are typing in. That requires the Accessibility API — AXUIElementCopyAttributeValue against the system-wide element — which requires the user to grant Accessibility permission.

So I did the normal thing at launch:

let trusted = AXIsProcessTrustedWithOptions(
    [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary)

Nothing happened. No permission dialog. Worse, the app never appeared in System Settings → Privacy & Security → Accessibility at all — so there was nothing to toggle on. Dragging the app in with the + button did not help either.

Every answer I found online says some version of “just add it manually in System Settings.” That advice assumes the app can be added. Mine could not.

Read the TCC logs instead of guessing

Permission decisions on macOS are made by tccd, and it is quite talkative in the unified log. This is the single most useful command I found:

log show --last 5m --style compact --predicate 'process == "tccd"' \
  | grep -i "<your bundle id>"

Two lines explained everything. The first appears on every launch, sandboxed or not:

tccd: AUTHREQ_CTX: msgID=..., service=kTCCServiceAccessibility, preflight=yes
tccd: Service kTCCServiceAccessibility does not allow prompting; returning Unknown

Accessibility does not prompt. On this version of macOS, calling AXIsProcessTrustedWithOptions with the prompt option does not produce a dialog for Accessibility, no matter how your app is built. The request reaches tccd and comes back Unknown.

That explains the missing dialog. It does not explain the missing list entry. The second line does — and this one only ever appeared for one of my builds:

tccd: Publishing <TCCDEvent: type=Modify,
      service=kTCCServiceAccessibility,
      identifier_type=Bundle ID,
      identifier=com.example.myapp> to 0 subscribers

This is TCC creating the record that makes an app appear in the Accessibility list. No event, no entry, nothing to toggle.

Isolating the cause

My first non-sandboxed build changed three things at once — I removed the sandbox, dropped get-task-allow, and switched from Debug to Release. The entry appeared, but I could not tell which change was responsible. get-task-allow is a plausible culprit: it marks a debug build, and TCC is known to be picky about signatures.

So I ran a proper one-variable test. Both builds were Release, both were ad-hoc signed, both had CODE_SIGN_INJECT_BASE_ENTITLEMENTS=NO so neither carried get-task-allow, and both were installed at the same path. Before each run I wiped the existing record so the starting state was identical:

tccutil reset Accessibility com.example.myapp

The only difference was com.apple.security.app-sandbox.

BuildSandboxget-task-allowTCC entry created
Aoffabsentyes — 0.03s after launch
Bonabsentno — 18s observed

Build B produced the same AUTHREQ_CTX and the same does not allow prompting line. It asked. TCC simply did not create a record for it.

What this means

A sandboxed app cannot be granted Accessibility permission, because it is never registered as a candidate. This is not a signing problem, not a missing entitlement you can add, and not something the user can work around by dragging the app into the list.

The practical consequence is a hard fork in the road, because the Mac App Store requires the sandbox:

Mac App StoreDirect distribution
App Sandboxrequiredoptional
Accessibility APIunavailableavailable
SigningMac App DistributionDeveloper ID + notarization

If your app needs to read another app’s UI — caret position, selected text, window contents — you are distributing outside the App Store. There is no configuration that gets you both.

Two things worth knowing before you build

Ad-hoc signatures lose the permission on every rebuild

TCC identifies an app by its code signature. An ad-hoc signature embeds the binary’s own hash, so every rebuild produces a different identity and the user’s grant stops applying. During development this looks like a bug that comes and goes.

A self-signed certificate fixes it. The designated requirement then references the certificate rather than the binary:

designated => identifier "com.example.myapp"
              and certificate leaf = H"80645c1d..."

Same requirement across rebuilds, so the grant survives. The certificate does not need to be trusted — referencing it by SHA-1 hash in codesign -s is enough for local builds.

Detect the sandbox at runtime and hide what cannot work

If you ship both variants from one codebase, let the app notice which one it is and hide the features it cannot deliver. Leaving a menu item that silently does nothing is worse than not having it:

func isSandboxed() -> Bool {
    if ProcessInfo.processInfo.environment["APP_SANDBOX_CONTAINER_ID"] != nil {
        return true
    }
    return NSHomeDirectory().contains("/Library/Containers/")
}

Summary

  • Accessibility never shows a permission prompt on macOS 26 — expect users to enable it manually.
  • A sandboxed app is not added to the Accessibility list at all, so it cannot be enabled.
  • Verified with a one-variable test: same Release build, same signing, sandbox on vs off.
  • Watch tccd in the unified log rather than guessing from the UI.
  • Use a stable signing identity in development or the grant will keep disappearing.

Comments

Popular posts from this blog

What ArduPilot SITL catches, and where it stops telling the truth

App Sandbox를 켜면 손쉬운 사용 권한을 받을 수 없다