From 9666b3f5f25bc088d5c21fc6b8e81c3a6bc6131e Mon Sep 17 00:00:00 2001 From: vipergsm Date: Fri, 31 Jul 2026 07:23:44 +0200 Subject: [PATCH 1/2] hotkey: re-enable the event tap when the system disables it macOS disables an event tap on .tapDisabledByTimeout or .tapDisabledByUserInput. The callback treated both as no-ops, so parrot kept running with a dead tap: the menu bar icon stayed put, the menu still read "idle - hold fn to dictate", and fn silently did nothing until the user noticed and restarted. For a daemon meant to sit in the menu bar for days that is a hard failure mode to diagnose. Re-enable the tap from the main run loop (the one that owns it) and log a line to stderr so the event is visible. Also narrow the mask. Only .flagsChanged is ever acted on -- handle() discards everything else on its first line -- but the tap subscribed to keyDown and keyUp as well, so every keystroke system-wide caused an event.copy() and a main-queue dispatch for nothing. Beyond the wasted work, that raises the odds of the very timeout above, and needlessly routes typed content (including password fields) through an Accessibility-privileged process. keyDown/keyUp are still subscribed under --debug-hotkey, where they are actually used. --- Sources/parrot/Input/HotkeyMonitor.swift | 30 +++++++++++++++++++----- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/Sources/parrot/Input/HotkeyMonitor.swift b/Sources/parrot/Input/HotkeyMonitor.swift index b3bc132c..e5743cf9 100644 --- a/Sources/parrot/Input/HotkeyMonitor.swift +++ b/Sources/parrot/Input/HotkeyMonitor.swift @@ -35,10 +35,17 @@ final class HotkeyMonitor { throw HotkeyError.tapCreateFailed } - let mask: CGEventMask = - (1 << CGEventType.flagsChanged.rawValue) - | (1 << CGEventType.keyDown.rawValue) - | (1 << CGEventType.keyUp.rawValue) + // Only flagsChanged is ever acted on in `handle`. keyDown/keyUp are + // subscribed *only* under --debug-hotkey: listening to every keystroke + // system-wide costs main-thread work, raises the odds macOS disables + // the tap for timeout, and needlessly exposes typed content (including + // password fields) to an Accessibility-privileged process. + var mask: CGEventMask = (1 << CGEventType.flagsChanged.rawValue) + if debug { + mask |= + (1 << CGEventType.keyDown.rawValue) + | (1 << CGEventType.keyUp.rawValue) + } let userInfo = Unmanaged.passUnretained(self).toOpaque() // .cgSessionEventTap is the right level for an accessibility-granted @@ -76,6 +83,17 @@ final class HotkeyMonitor { onEvent = nil } + /// Called from the tap callback when macOS disables our tap. Without this + /// the process keeps running, the menu bar icon stays put, and the hotkey + /// silently does nothing until the user restarts parrot. + fileprivate func reEnable() { + guard let tap else { return } + CGEvent.tapEnable(tap: tap, enable: true) + FileHandle.standardError.write(Data( + "event tap was disabled by the system — re-enabled\n".utf8 + )) + } + fileprivate func handle(type: CGEventType, event: CGEvent) { if debug { let flags = event.flags @@ -104,8 +122,8 @@ private func hotkeyCallback( let monitor = Unmanaged.fromOpaque(userInfo).takeUnretainedValue() if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput { - // System disabled our tap; we'll need to re-enable. For now just no-op - // and let the user restart parrot. + // The tap must be re-enabled from the run loop that owns it. + DispatchQueue.main.async { monitor.reEnable() } return Unmanaged.passUnretained(event) } From 8f01d036533316a62c8fa3e6e65cd8f93ece0869 Mon Sep 17 00:00:00 2001 From: vipergsm Date: Fri, 31 Jul 2026 07:23:44 +0200 Subject: [PATCH 2/2] install.sh: verify the published SHA256 before installing The release workflow already builds and uploads parrot-macos-arm64.tar.gz.sha256, but install.sh never fetched it. The script is invoked as `curl | sh`, downloads an unsigned binary, strips its quarantine attribute and moves it into /usr/local/bin -- with the checksum sitting unused two lines away. Fetch and verify it. --- scripts/install.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/install.sh b/scripts/install.sh index b4f09e14..070b81ae 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -62,6 +62,16 @@ trap 'rm -rf "$TMP"' EXIT dim "→ downloading ${ASSET}..." curl -fsSL "$URL" -o "$TMP/${ASSET}" +# The release workflow publishes ${ASSET}.sha256 alongside the archive. +# Verify it: this script installs an unsigned binary and strips its +# quarantine flag, so the checksum is the only integrity check there is. +dim "→ verifying checksum..." +curl -fsSL "${URL}.sha256" -o "$TMP/${ASSET}.sha256" +( cd "$TMP" && shasum -a 256 -c "${ASSET}.sha256" >/dev/null ) || { + red "checksum mismatch for ${ASSET} — refusing to install" + exit 1 +} + dim "→ extracting..." tar -xzf "$TMP/${ASSET}" -C "$TMP"