How the Accessibility Permission System Works with Event Taps in vorssaint‑utils

vorssaint‑utils requires two macOS privacy permissions—Accessibility and Screen Recording—to install low‑level event taps that intercept keyboard, mouse, and display events before the system processes them.

The accessibility permission system in vorssaint‑utils orchestrates detection, polling, and enforcement of macOS TCC (Transparency, Consent, and Control) grants. All global event taps that monitor user input rely on this centralized architecture, which lives in Sources/Vorssaint/Core/Permissions.swift and coordinates with individual service implementations like SuperKeyService and WindowLayoutService.

Core Permission Requirements

vorssaint‑utils distinguishes between two permission classes that gate different categories of event taps:

  • Accessibility (AXIsProcessTrusted) – Required for any tap that listens to raw HID input events (keyboard, mouse, trackpad) before system processing. All global input monitors depend on this grant.
  • Screen Recording (CGPreflightScreenCaptureAccess) – Required for taps that read screen contents, including window titles and thumbnails used by the window switcher and layout features.

Both permissions are managed by the Permissions singleton, which exposes @Published properties that SwiftUI views and background services observe to react instantly when grants are revoked or restored.

Detecting Current Permission Status

The system determines authorization state through cheap, side‑effect‑free CoreGraphics and Accessibility API calls that can run on a background queue. In Permissions.swift, the refreshActivePermissions() method polls both flags:

private func refreshActivePermissions() {
    let ax = AXIsProcessTrusted()               // Accessibility
    let sr = CGPreflightScreenCaptureAccess()   // Screen Recording
    DispatchQueue.main.async {
        if self.accessibility != ax { self.accessibility = ax }
        if self.screenRecording != sr { self.screenRecording = sr }
        // Re‑schedule the poll if the cadence changed
        self.scheduleActivePermissionPolling()
    }
}

AXIsProcessTrusted() returns a Boolean indicating whether the app appears in System Settings → Privacy & Security → Accessibility. CGPreflightScreenCaptureAccess() performs the equivalent check for Screen Recording. Because these calls are non‑blocking, vorssaint‑utils can poll them frequently without impacting UI responsiveness.

Conditional Polling Strategy

Rather than running a perpetual background timer, the accessibility permission system calculates a dynamic polling interval via desiredPollInterval (lines 82‑92 in Permissions.swift). The timer only activates when:

  1. A feature requiring the permission is currently active, or
  2. A UI surface displaying the permission state is visible.

If no active feature needs the grant, scheduleActivePermissionPolling() stops the timer entirely to eliminate unnecessary CPU usage. This lazy evaluation prevents vorssaint‑utils from draining battery while idle.

Requesting Permissions from the User

When a user enables a feature that requires Accessibility, the UI invokes requestAccessibility(), which triggers the system consent prompt:

func requestAccessibility() {
    let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary
    AXIsProcessTrustedWithOptions(options)                 // Shows the system prompt
    refreshActivePermissions()
    if !accessibility {
        PermissionGuideOverlay.shared.show(for: .accessibility)
    }
}

The system displays the native permission dialog once per TCC reset. The code immediately calls refreshActivePermissions() because the user might grant access without dismissing the dialog, allowing the app to enable the feature instantly. Screen Recording follows an identical pattern using CGRequestScreenCaptureAccess().

Reacting to Permission Changes

Permissions.swift registers two observers to detect external changes without polling:

  • Application activation – When the user returns from System Settings after manually toggling a permission, scheduleActivePermissionPolling() fires immediately to update state.
  • UserDefaults changes – Feature toggles that enable or disable permission UI surfaces trigger a cadence recalculation.

These observers ensure the app never waits for the next timer tick to discover that a user revoked Accessibility access in System Settings.

Guarding Event Tap Creation

Every service that installs a global or local event tap checks the shared permission state before calling CGEventTapCreate. For example, the Super‑Key service in Sources/Vorssaint/Services/SuperKey/SuperKeyService.swift uses a guard clause:

guard Permissions.shared.accessibility else {
    // Permission missing – show UI or abort tap installation
    return
}
eventTap = CGEventTapCreate(...)
// … configure and enable the tap

If the user revokes Accessibility while the tap is active, the next poll sets Permissions.shared.accessibility to false. The service’s Combine subscriber observes this change, tears down the CFMachPort, and optionally re‑displays the permission guide overlay.

Resetting Permissions for Development

The Permissions.startOver(_:) method supports advanced troubleshooting by invoking the tccutil command‑line tool to reset TCC entries for Accessibility or Screen Recording. This executes on a background queue via DispatchQueue.global() to keep the UI responsive while forcing macOS to show the consent prompt again on the next request.

Summary

  • Two permissions gate event taps: Accessibility for input interception, Screen Recording for display content reading.
  • Centralized detection: Permissions.refreshActivePermissions() queries AXIsProcessTrusted() and CGPreflightScreenCaptureAccess() without side effects.
  • Lazy polling: The timer only runs when needed, calculated by desiredPollInterval to conserve resources.
  • System integration: AXIsProcessTrustedWithOptions() displays the native consent dialog; observers on app activation catch manual changes instantly.
  • Defensive coding: Services like SuperKeyService guard CGEventTapCreate with Permissions.shared.accessibility checks and tear down taps immediately when grants disappear.

Frequently Asked Questions

What happens if I deny the Accessibility permission when vorssaint‑utils asks for it?

The app stores the denial but continues to function for features that do not require event taps. The specific service (such as Super‑Key) will remain inactive, and the UI will display PermissionGuideOverlay explaining how to manually grant access in System Settings. The next time you attempt to enable the feature, requestAccessibility() will trigger the system prompt again unless you have previously checked "Don't Ask Again" in macOS TCC settings.

Why does vorssaint‑utils need Screen Recording permission for window management features?

Window layout and switcher features use CGWindowListCopyWindowInfo or similar APIs to enumerate window titles and positions. Since macOS Catalina, any code that reads window titles or captures screen contents—including generating thumbnails—requires Screen Recording permission. The accessibility permission system checks CGPreflightScreenCaptureAccess() before installing taps that inspect kCGWindowName or kCGWindowBounds in Sources/Vorssaint/Services/WindowLayout/WindowLayoutService.swift.

How often does vorssaint‑utils poll for permission changes?

The polling cadence is dynamic. If a feature requiring Accessibility is active, scheduleActivePermissionPolling() typically sets a short interval (often 1–2 seconds) to detect revocations quickly. When no relevant features are active, the timer stops entirely. This logic prevents background CPU usage while ensuring the UI updates within seconds of a user changing permissions in System Settings.

Canvorssaint‑utils work partially without Accessibility permission?

Yes. Features that do not rely on low‑level event taps—such as timers, UI themes, or file utilities—function normally. Only services that call CGEventTapCreate, like the scroll inverter or keyboard remapper, require the grant. Each service independently checks Permissions.shared.accessibility before attempting to install its tap, ensuring the app never crashes from unauthorized API calls.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →