The Vanishing Keystrokes Bug
How a faceless background agent ate my typing, and the script that caught it.
A few days into an uptime, deep in something, my Mac would stop listening. Not a crash, not a freeze. The cursor still moved, windows still highlighted under it, I could click anything. But the keyboard went dead: I'd type a whole sentence into a window that looked focused and watch zero characters land. Clicking didn't help. Sometimes it cleared after a few seconds; sometimes I rebooted and bought another day or two of quiet.
A bug that only shows up after hours of uptime, never on a cold boot, and clears on reboot is the worst kind. You can't reproduce it on demand, so you can't poke at it. I finally cornered it, and the culprit wasn't what I assumed.
The Symptoms
If your Mac does this too, start here. The shape of the failure tells you where to look.
- The mouse works. The keyboard doesn't. Pointer moves, clicks land, but no window accepts text.
- It's system-wide, not one app. Every window is dead, whichever one you click into.
- It builds up over uptime. Fine on a fresh boot, more frequent the longer the machine stays awake.
- A reboot fixes it. Temporarily. It always comes back.
- The focused window looks slightly de-focused, title bar greyed out, as if nothing is frontmost.
That combination rules a lot out. It isn't a hardware keyboard fault: the mouse and keyboard share enough of the input stack that a real HID failure takes both. It isn't one misbehaving app. It points at the part of the OS that decides where keystrokes go, and at something that corrupts that decision the longer you stay logged in.
Two Kinds of "Front"
macOS tracks "in front" in two places that are supposed to agree.
- The active application (LaunchServices and
NSWorkspace). Apple definesfrontmostApplicationbluntly: the app that receives key events.lsappinforeports it. - The key window (WindowServer). Within the active app, keys flow to the key window, then to its first responder, the text field your cursor sits in. No window that can become key means no key window, and nowhere for text to land.
A third notion, Accessibility's AXFrontmost (what AppleScript's System Events reports), tracks the app whose UI is actually up front.
On a healthy Mac all three agree and you never think about it. The bug lives in the gap: if LaunchServices says one app is active while Accessibility says another, the OS routes your keystrokes to an app you aren't looking at. If that app has no window, they evaporate.
Bug Catcher
Check both notions from the terminal, especially while the bug is happening:
# LaunchServices: who owns key events?
lsappinfo info -only name "$(lsappinfo front)"
# Accessibility: who's actually up front?
osascript <<'EOF'
tell application "System Events"
name of first application process whose frontmost is true
end tell
EOF
Healthy, these match. Mine didn't:
LaunchServices : "Logitech G HUB Agent"
Accessibility : wezterm-gui
LaunchServices thought a faceless agent, the Logitech G HUB Agent, was the active app receiving key events, while the app I was clicking into was my terminal. My keystrokes were routing to an agent with no window.
Two commands catch it if your timing is lucky. The bug is intermittent, so I wrote a monitor that shouts the moment the two disagree.
focus-spy
It polls both notions once a second, logs every change, and flags MISMATCH when the PIDs differ. Drop it in your $PATH, chmod +x it, run it.
#!/usr/bin/env bash
# focus-spy - find out what's stealing keyboard focus on macOS.
#
# macOS tracks "the frontmost app" in two places that should agree:
# * LaunchServices / NSWorkspace - the app that RECEIVES KEY EVENTS
# (what `lsappinfo front` reports)
# * Accessibility (AXFrontmost) - the app whose UI is up front
# (what System Events reports)
# When a background agent shoves itself into the first one without a
# real window, the two disagree, and your keystrokes fall in the gap.
# This logs both once a second and shouts when their PIDs differ.
#
# Usage:
# focus-spy # watch (Ctrl-C to stop)
# focus-spy mark NOTE # mark the instant typing dies
# focus-spy report # print the timeline, sorted
set -uo pipefail
LOG="${FOCUS_SPY_LOG:-$HOME/.local/state/focus-spy.log}"
mkdir -p "$(dirname "$LOG")"
ts() { date '+%Y-%m-%d %H:%M:%S'; }
# LaunchServices' frontmost (the key-event owner) as "name#pid".
# Compare by PID, not name: the two APIs spell the same app
# differently ("WezTerm" vs "wezterm-gui"); only the PID is identity.
ls_front() {
local asn name pid
asn="$(lsappinfo front 2>/dev/null)"
name="$(lsappinfo info -only name "$asn" 2>/dev/null)"
name="${name#*=}"; name="${name//\"/}"
pid="$(lsappinfo info -only pid "$asn" 2>/dev/null)"
pid="${pid##*=}"
printf '%s\n' "${name:-?}#${pid:-?}"
}
# Accessibility's frontmost (the visible app) as "name#pid". First run
# may prompt your terminal for Automation access to System Events.
ax_front() {
osascript 2>/dev/null <<'OSA'
tell application "System Events"
set p to first application process whose frontmost is true
return (name of p) & "#" & (unix id of p)
end tell
OSA
}
watch() {
echo "focus-spy: watching (Ctrl-C to stop). Log: $LOG"
local prev="" ls ax line tag
while :; do
ls="$(ls_front)"; ax="$(ax_front)"
line="ls=[$ls] ax=[$ax]"
if [[ "$line" != "$prev" ]]; then
[[ "${ls##*#}" == "${ax##*#}" ]] && tag="ok " || tag="MISMATCH"
printf '%s\n' "$(ts) $tag $line" | tee -a "$LOG"
prev="$line"
fi
sleep 1
done
}
case "${1:-watch}" in
watch) watch ;;
mark) shift
printf '%s\n' "$(ts) MARK >>> ${*:-typing died} <<<" \
| tee -a "$LOG" ;;
report) sort "$LOG" 2>/dev/null || echo "no log yet" ;;
*) echo "usage: focus-spy [watch|mark NOTE|report]"; exit 1 ;;
esac
Leave focus-spy watch running in a spare terminal. The instant the keyboard dies, run focus-spy mark "typing died" in any shell, then focus-spy report. Look for a MISMATCH line: whatever sits on the ls= side is your thief. Every one of mine read ls=[Logitech G HUB Agent].

The two fronts diverging on demand. A faceless stand-in agent (built to reproduce the bug) seizes the LaunchServices slot while the terminal keeps Accessibility focus, so the status flips to MISMATCH, then back the instant it exits.
The Smoking Gun
A monitor tells you who; a kill test tells you whether you're right. The LaunchServices front process read Logitech G HUB Agent more than twenty samples in a row. So I killed the stack and watched it the instant it died:
killall lghub_agent lghub_system_tray lghub
lsappinfo info -only name "$(lsappinfo front)"
The active app snapped back to WezTerm and held. Both notions agreed. Typing was solid. G HUB's agent had been parking itself in the active-app slot and never letting go.
Why It Breaks
The active app receives key events, routed to its key window's first responder. An agent app (LSUIElement, no Dock icon) can still become the active app, and the old SetFrontProcess API was deprecated for NSRunningApplication.activate, which developers have long reported is easy to leave in an inconsistent state. G HUB falls into exactly that: it activates itself, probably an unbalanced activate on a device-poll loop, and becomes the active app without a window that can become key. The keyboard now points at a process with no first responder, so keystrokes are dropped, not stolen, just discarded, until you click into a real app. The mouse still works because mouse events are hit-tested by pointer location, not by which app is active. (Don't confuse it with Secure Input, where an app legitimately swallows every keystroke and forgets to stop; there both notions of "front" still agree. The mismatch is the tell.)
You're not imagining it, either. Another engineer built the same kind of monitor and clocked G Hub grabbing focus 47 times in four minutes. A MacRumors thread nails it: G Hub "keeps trying to get focus ... since it only runs in the background and has no window to receive the focus, the frontmost app of the system loses focus." Logi Options+ does the same. The trigger looks like wake and device re-enumeration, which fits why it snowballs over uptime and resets on reboot. The lesson: when you finally name a weird bug, search the name. You're rarely the first to hit a real defect.
The Fix
Right now, quit it:
killall lghub_agent lghub_system_tray lghub
Permanently: a launch agent restarts it at login (/Library/LaunchAgents/com.logi.ghub.plist, RunAtLoad). Move it aside, reversible:
sudo mv /Library/LaunchAgents/com.logi.ghub.plist \
~/Documents/backup/com.logi.ghub.plist
Then check System Settings, General, Login Items & Extensions for any Logitech entry. If you don't use Logitech G gaming gear, uninstall G HUB outright; Logi Options+ already covers a normal mouse.
My Mac types again. Now when focus feels off, I reach for focus-spy watch in a corner terminal, because the next thief trips the same wire.