From a8da25d987a5cf61b6c099c21e1ee4d1d47a43d9 Mon Sep 17 00:00:00 2001 From: Jason Titus <870238+jasontitus@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:32:52 -0700 Subject: [PATCH] Fix Input Monitoring permission flow on macOS 10.15.6+ (#166) Since macOS 10.15.6, CGEventTap-based key filtering requires both Accessibility AND Input Monitoring permission, but the app only ever prompted for Accessibility. The TCC system never asked the user to grant Input Monitoring, so the event tap silently no-op'd even after they granted Accessibility, leaving the impression that Unshaky was broken. On launch, call IOHIDRequestAccess(kIOHIDRequestTypeListenEvent) to trigger the Input Monitoring TCC prompt the first time it's needed, then verify with IOHIDCheckAccess. If permission is missing, show a dedicated help alert and open System Settings -> Privacy & Security -> Input Monitoring directly (Privacy_ListenEvent pane), separate from the existing Accessibility flow. Also gets the project building on Xcode 26 / current macOS: - Bump deployment target 10.12 -> 10.13 (Xcode 26 minimum) - Remove hardcoded "Apple Development: Xinhong Liu" identity from the Sparkle re-sign script phase, which forced every other developer's build to fail at codesign --- Podfile | 2 +- Unshaky.xcodeproj/project.pbxproj | 6 ++--- Unshaky/AppDelegate.swift | 34 +++++++++++++++++++++++++- Unshaky/Base.lproj/Localizable.strings | 2 ++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/Podfile b/Podfile index 066584b..563c27f 100644 --- a/Podfile +++ b/Podfile @@ -1,5 +1,5 @@ # Uncomment the next line to define a global platform for your project -platform :osx, '10.12' +platform :osx, '10.13' target 'Unshaky' do # Comment the next line if you're not using Swift and don't want to use dynamic frameworks diff --git a/Unshaky.xcodeproj/project.pbxproj b/Unshaky.xcodeproj/project.pbxproj index e6b6f4f..8aa32fa 100644 --- a/Unshaky.xcodeproj/project.pbxproj +++ b/Unshaky.xcodeproj/project.pbxproj @@ -427,7 +427,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "LOCATION=\"${BUILT_PRODUCTS_DIR}\"/\"${FRAMEWORKS_FOLDER_PATH}\"\n\n# By default, use the configured code signing identity for the project/target\nIDENTITY=\"${CODE_SIGN_IDENTITY}\"\nIDENTITY=\"Apple Development: Xinhong Liu (32BU6UVQ76)\"\nif [ \"$IDENTITY\" == \"\" ]\nthen\n# If a code signing identity is not specified, use ad hoc signing\nIDENTITY=\"-\"\nfi\nif [ \"$DONT_CODE_SIGN\" == \"YES\" ]\nthen\n# If a code signing identity is not specified, use ad hoc signing\nIDENTITY=\"-\"\nfi\ncodesign --verbose --force --deep -o runtime --sign \"$IDENTITY\" \"$LOCATION/Sparkle.framework/Versions/A/Resources/AutoUpdate.app\"\ncodesign --verbose --force -o runtime --sign \"$IDENTITY\" \"$LOCATION/Sparkle.framework/Versions/A\"\n"; + shellScript = "LOCATION=\"${BUILT_PRODUCTS_DIR}\"/\"${FRAMEWORKS_FOLDER_PATH}\"\n\n# By default, use the configured code signing identity for the project/target\nIDENTITY=\"${CODE_SIGN_IDENTITY}\"\nif [ \"$IDENTITY\" == \"\" ]\nthen\n# If a code signing identity is not specified, use ad hoc signing\nIDENTITY=\"-\"\nfi\nif [ \"$DONT_CODE_SIGN\" == \"YES\" ]\nthen\n# If a code signing identity is not specified, use ad hoc signing\nIDENTITY=\"-\"\nfi\ncodesign --verbose --force --deep -o runtime --sign \"$IDENTITY\" \"$LOCATION/Sparkle.framework/Versions/A/Resources/AutoUpdate.app\"\ncodesign --verbose --force -o runtime --sign \"$IDENTITY\" \"$LOCATION/Sparkle.framework/Versions/A\"\n"; }; 63A7B0E478BADB58754CFBEC /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -792,7 +792,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.12; + MACOSX_DEPLOYMENT_TARGET = 10.13; MARKETING_VERSION = 0.7.0; PRODUCT_BUNDLE_IDENTIFIER = com.nestederror.Unshaky; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -821,7 +821,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.12; + MACOSX_DEPLOYMENT_TARGET = 10.13; MARKETING_VERSION = 0.7.0; PRODUCT_BUNDLE_IDENTIFIER = com.nestederror.Unshaky; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Unshaky/AppDelegate.swift b/Unshaky/AppDelegate.swift index ede9dc0..12094b0 100644 --- a/Unshaky/AppDelegate.swift +++ b/Unshaky/AppDelegate.swift @@ -8,6 +8,7 @@ import Cocoa import ServiceManagement +import IOKit.hid @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { @@ -88,17 +89,48 @@ class AppDelegate: NSObject, NSApplicationDelegate { // Basic // func setup() { + // Since macOS 10.15 (and strictly enforced in 10.15.6+), CGEventTap requires + // BOTH Accessibility AND Input Monitoring permission. Trigger the Input + // Monitoring prompt up-front so the user lands in the right pane. + let inputMonitoringGranted = requestInputMonitoringIfNeeded() + // this following lines will add Unshaky.app to privacy->accessibility panel, unchecked let checkOptPrompt = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString let accessEnabled = AXIsProcessTrustedWithOptions([checkOptPrompt: false] as CFDictionary?) - if (!shakyPressPreventer.setupEventTap() || !accessEnabled) { + let eventTapOK = shakyPressPreventer.setupEventTap() + + if !accessEnabled || !eventTapOK { let alert = NSAlert() alert.messageText = NSLocalizedString("Accessibility Help", comment: "") alert.runModal() NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")!) NSApplication.shared.terminate(self) + return + } + + if !inputMonitoringGranted { + let alert = NSAlert() + alert.messageText = NSLocalizedString("Input Monitoring Help", comment: "") + alert.runModal() + NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent")!) + NSApplication.shared.terminate(self) + } + } + + /// Returns true if Input Monitoring is granted (or can't be queried on this OS). + /// On macOS 10.15+, also triggers the system prompt the first time it's needed. + private func requestInputMonitoringIfNeeded() -> Bool { + if #available(macOS 10.15, *) { + let access = IOHIDCheckAccess(kIOHIDRequestTypeListenEvent) + if access == kIOHIDAccessTypeGranted { + return true + } + // Fires the TCC prompt on first call; subsequent calls just return the + // current state without re-prompting. + return IOHIDRequestAccess(kIOHIDRequestTypeListenEvent) } + return true } func recover() { diff --git a/Unshaky/Base.lproj/Localizable.strings b/Unshaky/Base.lproj/Localizable.strings index c3ec6ed..d8500ec 100644 --- a/Unshaky/Base.lproj/Localizable.strings +++ b/Unshaky/Base.lproj/Localizable.strings @@ -14,6 +14,8 @@ "Accessibility Help" = "Unshaky needs accessibility permission to work. Right now, Unshaky will attempt to open this panel for you. If it does not open automatically, you have to go to: System Preferences -> Security & Privacy -> Privacy (Tab) -> Accessibility (Left panel). \n\nWhen you have the panel open, add and check the Unshaky.app. \n\nOn Big Sur, you will need to do the same for Privacy (Tab) -> Input Monitoring (Left panel)."; +"Input Monitoring Help" = "Unshaky also needs Input Monitoring permission on macOS 10.15.6 and later. Unshaky will attempt to open this panel for you. If it does not open automatically, go to: System Settings -> Privacy & Security -> Input Monitoring, then add and enable Unshaky.app. \n\nAfter enabling it, relaunch Unshaky."; + "Invalid Delay" = "\"%@\" is not a valid delay. Please input an integer value."; "Paste the configuration here" = "Paste the configuration here";