#1419·glazewm

[Bug] macOS: keybindings permanently stop working after sleep/wake — event tap is never re-enabled after kCGEventTapDisabledByTimeout

Author: shyam-kingCreated Aug 12, 2026Updated Aug 12, 2026

DISCLAIMER: I filed this issue using AI. I lack the context of the project at the moment to validate the proposed fix otherwise would have raised a PR directly.

Describe the bug

On macOS, all keybindings can suddenly and permanently stop responding while the rest of GlazeWM keeps working normally (IPC commands via glazewm command ... still work, window management still works, the process is healthy and idle). Restarting GlazeWM restores keybindings.

Possible root cause (details below): macOS disables a CGEventTap when its callback is too slow to respond — most commonly around sleep/wake — and notifies the callback with a kCGEventTapDisabledByTimeout event so the app can re-enable it. GlazeWM's keyboard hook never handles that event type, so once the tap is disabled it stays disabled until the process is restarted.

To reproduce

  1. Run GlazeWM on macOS for an extended period (with sleep/wake cycles; in my case it broke after ~1.5 days of uptime that included several lid-close sleeps and lock-screen logins).
  2. At some point after a wake, keybindings stop responding entirely — every binding, not a subset.
  3. Observe that GlazeWM is otherwise healthy:
    • glazewm query pausedfalse
    • glazewm query binding-modes[]
    • glazewm command focus --workspace 2 → works fine
    • no new entries in ~/.glzr/glazewm/errors.log
  4. wm-exit + relaunch → keybindings work again.

Expected behavior

Keybindings keep working across sleep/wake, or at minimum the tap is re-enabled when macOS disables it, per Apple's CGEventTapCallBack contract.

Possible root cause

packages/wm-platform/src/platform_impl/macos/keyboard_hook.rs (same on main and v3.10.1):

When macOS disables a tap, it delivers one final event of type kCGEventTapDisabledByTimeout (0xFFFFFFFE) or kCGEventTapDisabledByUserInput (0xFFFFFFFF) to the callback — bypassing the event mask — and expects the app to call CGEventTapEnable(tap, true) to resume: https://developer.apple.com/documentation/coregraphics/cgeventtapcallback

keyboard_event_callback never checks event_type for these sentinels. It goes straight to reading the keycode field (garbage for a disable event), fails the Key::try_from conversion, and passes the event through — silently discarding the "your tap was just disabled" notification:

rust
extern "C-unwind" fn keyboard_event_callback(
    _proxy: CGEventTapProxy,
    event_type: CGEventType,
    mut event: NonNull<CGEvent>,
    user_info: *mut c_void,
) -> *mut CGEvent {
    // ... no check for TapDisabledByTimeout / TapDisabledByUserInput ...

    // Extract the key code of the pressed/released key.
    let key_code = KeyCode(unsafe {
        CGEvent::integer_value_field(
            Some(event.as_ref()),
            CGEventField::KeyboardEventKeycode,
        )
    });

    // Try to convert the key code to a known key.
    let Ok(key) = Key::try_from(key_code) else {
        return unsafe { event.as_mut() };  // <- disable notification ends here
    };

The only CGEvent::tap_enable(&tap_port, true) call in the file is the one at tap creation, so nothing ever re-enables it.

mouse_listener.rs has the same callback structure and presumably the same issue (a disabled mouse tap would break focus_follows_cursor), though I've only observed the keyboard symptom.

Suggested fix

Handle the disable sentinels at the top of the callback and re-enable the tap:

rust
if event_type == CGEventType::TapDisabledByTimeout
    || event_type == CGEventType::TapDisabledByUserInput
{
    CGEvent::tap_enable(&tap_port, true);
    return unsafe { event.as_mut() };
}

The tap port isn't currently reachable from the callback, so it would need to be stored in CallbackData (e.g. set after tap_create succeeds, before events start flowing).

For TapDisabledByUserInput specifically, re-enabling immediately is also what other long-lived taps (Hammerspoon, Karabiner) do — macOS sends it around secure-input transitions and the tap should resume afterwards.

Environment

  • GlazeWM v3.10.1 (Homebrew cask)
  • macOS (Darwin 25.5.0, Apple Silicon, MacBookPro18,3)
  • Single built-in display

Workaround for anyone else hitting this

glazewm command wm-exit, then relaunch GlazeWM. Only key capture dies; everything else survives, so a restart is cheap.