global-hotkeys
SKILL.md
Global Hotkeys — System-Wide Keyboard Shortcuts
Critical Constraints
- ❌ DO NOT use only
addGlobalMonitorForEvents→ ✅ Also addaddLocalMonitorForEvents(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.deviceIndependentFlagsMaskfirst - ❌ DO NOT use NSEvent for system-wide hotkeys in sandboxed apps → ✅ Use Carbon
RegisterEventHotKeyfor 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
Repository
makgunay/claude…t-skillsFirst Seen
Feb 14, 2026
Security Audits
Installed on
opencode5
claude-code5
codex5
cursor5
gemini-cli4
github-copilot4