Skip to content

fix(hotkey): invalidate CGEventTap Mach port on teardown to stop system-wide tap leak (#488)#499

Merged
silverstein merged 2 commits into
mainfrom
fix/cgeventtap-leak
Jul 17, 2026
Merged

fix(hotkey): invalidate CGEventTap Mach port on teardown to stop system-wide tap leak (#488)#499
silverstein merged 2 commits into
mainfrom
fix/cgeventtap-leak

Conversation

@silverstein

Copy link
Copy Markdown
Owner

Fixes #488 — a long-running app accumulates 512+ disabled CGEventTaps and breaks screenshots / Screen Recording system-wide.

Root cause

run_event_tap (crates/core/src/hotkey_macos.rs) tears down with CGEventTapEnable(false) + CFRelease but never calls CFMachPortInvalidate. Disabling and releasing the CFMachPort does not remove a CGEventTapCreate port from the system's 512-entry event-tap table — the canonical teardown must invalidate the Mach port first. So each tap is left as a disabled orphan.

The churn comes from the permission monitor: for users who have granted Input Monitoring, the 15s spawn_permission_monitor poll runs an active tap probe (input_monitoring_runtime_issueprobe_hotkey_monitorCGEventTapCreate). 512 orphans × 15s ≈ 2.1h of uptime to exhaust the table, matching the report.

Fix

Declare CFMachPortInvalidate in the FFI block and call it at teardown (after CFRunLoopRemoveSource, before CFRelease). Every tap — probe, dictation monitor, and each register/unregister cycle — is now actually removed, not just dereferenced. This is a complete fix for the leak with no behavior change.

Recommended follow-up (separate)

The passive 15s poll should not create a real tap at all — read Input Monitoring state via the tap-free CGPreflightListenEventAccess() and reserve the active "can I start a tap" probe for explicit diagnostics. That removes ~5,760 unnecessary tap create/teardown cycles/day (now harmless, but wasteful).

Reported by @jmh1313.

🤖 Generated with Claude Code

…#488)

run_event_tap tore down with CGEventTapEnable(false) + CFRelease but never
called CFMachPortInvalidate, so each tap was left registered in the
system's 512-entry event-tap table as a disabled orphan. Repeated
create/teardown cycles -- the input-monitoring permission probe (spawned
by the 15s permission monitor for Input-Monitoring-granted users) and
hotkey re-registration -- accumulate orphans until the table is exhausted,
after which any tap consumer including Screen Recording / screenshots
fails system-wide.

Declare CFMachPortInvalidate in the FFI block and call it at teardown
(after CFRunLoopRemoveSource, before CFRelease). Follow-up worth doing
separately: stop the passive 15s permission poll from creating a real tap
at all (read state via CGPreflightListenEventAccess and reserve the active
probe for explicit diagnostics) to avoid the churn even though it no
longer leaks.

Reported by @jmh1313.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
useminutes.app Ready Ready Preview, Comment Jul 17, 2026 6:48am

…ath (#488)

The source.is_null() error branch releases the tap that CGEventTapCreate
already registered but did not invalidate it, leaking a registered orphan
on that (rare) path too. Add CFMachPortInvalidate there for completeness,
matching the teardown fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@silverstein
silverstein merged commit 1f89523 into main Jul 17, 2026
24 checks passed
silverstein added a commit that referenced this pull request Jul 17, 2026
…15s (#488) (#500)

input_monitoring_runtime_issue() created and tore down a real
kCGHIDEventTap on every 15s permission-monitor poll (for users who granted
Input Monitoring) just to detect a "granted but unusable" state. That state
is fixed for the process lifetime -- it's only cleared by restarting the
app -- so re-probing is pointless churn on the system-wide input event
path. Before #488's teardown fix this churn leaked taps until the tap table
exhausted and keyboard/mouse froze system-wide (WindowServer could crash).

Memoize the probe result so it runs exactly once per launch, preserving the
diagnostic with zero ongoing churn. Complements the CFMachPortInvalidate
teardown fix (#499).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
silverstein added a commit that referenced this pull request Jul 17, 2026
Patch release. Bugfixes only.

- fix(hotkey): stop the system-wide CGEventTap leak — a permission probe
  created a kCGHIDEventTap every 15s and never invalidated it, exhausting
  the system tap table and freezing keyboard/mouse (and crashing
  WindowServer) after the app ran a few hours; quitting Minutes restored
  performance. Now invalidated on teardown and probed once per process.
  (#488, #499, #500)
- fix(build): pin the Swift helper deployment targets so 0.22.0's
  native-call-capture helper loads on macOS 15 instead of dyld-aborting.
  (#494, #497)
- fix(core): let `--call` auto-detect PipeWire system-audio sinks on Linux.
  (#489, thanks @gtheys)
- fix(summarize): support the Apple engine for speaker mapping. (#487, #498)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CGEventTap leak: long-running app accumulates 512+ disabled taps, exhausting the system tap table (breaks screenshots system-wide)

1 participant