global-hotkeys

SKILL.md

Global Hotkeys — System-Wide Keyboard Shortcuts

Critical Constraints

  • ❌ DO NOT use only addGlobalMonitorForEvents → ✅ Also add addLocalMonitorForEvents (global doesn't fire when YOUR app is focused)
  • ❌ DO NOT forget Accessibility permission → ✅ Global event monitoring requires AXIsProcessTrusted()
  • ❌ DO NOT compare raw modifierFlags → ✅ Mask with .deviceIndependentFlagsMask first
  • ❌ DO NOT use NSEvent for system-wide hotkeys in sandboxed apps → ✅ Use Carbon RegisterEventHotKey for reliability

Decision Tree

In-app shortcut only?
└── SwiftUI .onKeyPress or .keyboardShortcut

System-wide, non-sandboxed?
├── Simple → NSEvent global + local monitors
└── Reliable → Carbon RegisterEventHotKey

System-wide, sandboxed (App Store)?
└── Carbon RegisterEventHotKey + Accessibility entitlement

Method 1: NSEvent Monitors (Simple)

import AppKit

class HotkeyManager {
    private var globalMonitor: Any?
    private var localMonitor: Any?
    var onHotkey: (() -> Void)?
    var modifiers: NSEvent.ModifierFlags = [.command, .shift]
    var keyCode: UInt16 = 49  // Space

    func start() {
        globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { [weak self] event in
            self?.handleEvent(event)
        }
        localMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
            self?.handleEvent(event)
            return event
        }
    }

    func stop() {
        if let m = globalMonitor { NSEvent.removeMonitor(m) }
        if let m = localMonitor { NSEvent.removeMonitor(m) }
    }

    private func handleEvent(_ event: NSEvent) {
        let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
        if flags == modifiers && event.keyCode == keyCode {
            DispatchQueue.main.async { self.onHotkey?() }
        }
    }
}

Method 2: Carbon API (Reliable, System-Wide)

import Carbon

class CarbonHotkeyManager {
    private var hotkeyRef: EventHotKeyRef?
    private var eventHandler: EventHandlerRef?
    var onHotkey: (() -> Void)?

    func register(keyCode: UInt32, modifiers: UInt32) {
        unregister()
        var hotkeyID = EventHotKeyID()
        hotkeyID.signature = OSType("HTKY".fourCharCodeValue)
        hotkeyID.id = 1

        var eventType = EventTypeSpec(
            eventClass: OSType(kEventClassKeyboard),
            eventKind: UInt32(kEventHotKeyPressed)
        )

        let handler: EventHandlerUPP = { _, event, userData in
            let mgr = Unmanaged<CarbonHotkeyManager>.fromOpaque(userData!).takeUnretainedValue()
            mgr.onHotkey?()
            return noErr
        }

        InstallEventHandler(GetApplicationEventTarget(), handler, 1, &eventType,
                           Unmanaged.passUnretained(self).toOpaque(), &eventHandler)
        RegisterEventHotKey(keyCode, modifiers, hotkeyID,
                           GetApplicationEventTarget(), 0, &hotkeyRef)
    }

    func unregister() {
        if let ref = hotkeyRef { UnregisterEventHotKey(ref) }
        if let h = eventHandler { RemoveEventHandler(h) }
    }
}

extension String {
    var fourCharCodeValue: FourCharCode {
        utf8.reduce(0) { ($0 << 8) + FourCharCode($1) }
    }
}

Carbon Modifier Constants

// Carbon modifier flags for RegisterEventHotKey
let cmdKey: UInt32    = UInt32(cmdKey)      // 256
let shiftKey: UInt32  = UInt32(shiftKey)    // 512
let optionKey: UInt32 = UInt32(optionKey)   // 2048
let controlKey: UInt32 = UInt32(controlKey) // 4096

// Example: Cmd+Shift+Space
register(keyCode: 49, modifiers: UInt32(cmdKey) | UInt32(shiftKey))

Common Key Codes

Key Code Key Code Key Code
Space 49 Return 36 Escape 53
Tab 48 Delete 51 A 0
S 1 D 2 F 3
J 38 K 40 L 37

SwiftUI In-App Shortcuts

// Keyboard shortcut on button
Button("Save") { save() }
    .keyboardShortcut("s", modifiers: .command)

// Raw key press handler
.onKeyPress(.escape) { dismiss(); return .handled }
.onKeyPress(.upArrow) { moveUp(); return .handled }
.onKeyPress(.downArrow) { moveDown(); return .handled }

Accessibility Permission Check

func checkAccessibilityPermissions() -> Bool {
    let options = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as String: true]
    return AXIsProcessTrustedWithOptions(options as CFDictionary)
}

// Non-prompting check
func isAccessibilityGranted() -> Bool {
    AXIsProcessTrusted()
}

Common Mistakes & Fixes

Mistake Fix
Hotkey works everywhere except own app Add local monitor alongside global monitor
Modifier comparison fails Mask with .deviceIndependentFlagsMask
Hotkey fires twice Debounce with timestamp check (0.3s threshold)
Doesn't work on first launch Check/request Accessibility permission

References

Weekly Installs
5
First Seen
Feb 14, 2026
Installed on
opencode5
claude-code5
codex5
cursor5
gemini-cli4
github-copilot4