See exactly what your keyboard sends to macOS, live in your terminal.
- Watch every key down, key up, repeat and modifier change as macOS receives it
- See how long each key was held and how many times it repeated
- Spot events that other apps inject, and which process sent them
- Debug ZMK and QMK macros, home row mods, remappers and stuck modifiers
Note
keywatch runs on macOS 12 or later, on Apple silicon and Intel. It needs Input Monitoring for your terminal app.
brew install reemus-dev/tap/keywatchmise use -g github:reemus-dev/keywatchDownload the archive for your Mac from the
GitHub Releases page, then
copy keywatch to a directory on PATH.
The binaries are not notarized. If macOS blocks a binary you downloaded with a browser, remove the quarantine flag:
xattr -d com.apple.quarantine keywatchRequires the Xcode Command Line Tools.
git clone https://github.com/reemus-dev/keywatch.git
cd keywatch
swift build -c releaseThe binary is written to .build/release/keywatch.
Important
macOS only shows key events to apps with Input Monitoring. The permission belongs to the terminal app that runs keywatch (Terminal, iTerm2, Ghostty, VS Code and so on), not to keywatch itself.
On first run, keywatch asks macOS for access and exits with instructions:
- Open System Settings › Privacy & Security › Input Monitoring.
- Turn on your terminal app.
- Quit and reopen the terminal app, then run
keywatchagain.
keywatch does not need Accessibility access. It only listens, and cannot
change, block or send events.
While secure input is on, macOS hides keystrokes from every app that listens,
including keywatch. Password fields and the "Secure Keyboard Entry" option in
some terminals turn it on.
keywatch # watch key events, Ctrl-C to stop
keywatch --out keys.log # also save the lines as plain text
keywatch | grep F19 # piped output is plain text| Flag | Meaning |
|---|---|
--out <file> |
Also save every line, as plain text, to file. |
-h, --help |
Show help. |
-V, --version |
Show the version. |
You can type straight into the terminal that runs keywatch. It turns off echo
while it runs, and discards what you typed when it stops. It also sees keys you
press in any other app.
time gap key event ⌃⌥⇧⌘ held
17:15:07.889 left ⇧ 56 ▼ down ··⇧·
17:15:07.929 +40 F19 80 ▼ down ··⇧·
17:15:08.330 +401 F19 80 ↻ repeat ··⇧·
17:15:09.207 +877 F19 80 ▲ up ··⇧· 1278 ms ▄▄▄▄▄▄▄▄▄▄▄▄ 9 repeats
17:15:09.210 +3 left ⇧ 56 ▲ up ···· 1317 ms ▄▄▄▄▄▄▄▄▄▄▄▄▄
┄┄ 2.7 s idle ┄┄
17:15:11.952 +2742 v 9 ▲ up ···⌘ 19 ms ▄ ⚠ posted by VoiceInk (1021)
| Column | Meaning |
|---|---|
| time | Wall-clock time the event arrived |
| gap | Milliseconds since the previous event |
| key | Key name in your current keyboard layout, then the macOS key code |
| event | ▼ down, ▲ up, ↻ repeat (first repeat only), caps lock ● on and ○ off |
| ⌃⌥⇧⌘ | Modifiers active for the event, plus fn |
| held | On key up: hold time, a bar with one block per 100 ms up to 2 s, and the total repeat count |
⚠ posted by <process> (<pid>)marks events that a process sent rather than a keyboard, such as a remapper, a dictation app or automation.┄┄ idle ┄┄separates bursts more than 2 seconds apart.- A live bottom line lists every key held right now, for example
● holding left ⌘ 1.2s 1 0.9s ↻12.
A ZMK macro held for about 2 seconds (abridged):
key event ⌃⌥⇧⌘ held
left ⌘ 55 ▼ down ···⌘
1 18 ▼ down ···⌘
1 18 ▲ up ···⌘ 83 ms ▄
left ⌘ 55 ▲ up ···· 1917 ms ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
The 1 lasts 83 ms however long the key is held, so an app that waits for a
held key never sees one. After changing the macro to press the key, pause for
release, then release it, both keys last the full hold. macOS repeats the 1,
as it would on any keyboard:
left ⌘ 55 ▼ down ···⌘
1 18 ▼ down ···⌘
1 18 ↻ repeat ···⌘
1 18 ▲ up ···⌘ 1693 ms ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ 48 repeats
left ⌘ 55 ▲ up ···· 1706 ms ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
keywatch installs a listen-only
Quartz event tap
at the head of the HID event stream. It sees key events where they enter the
window server, before hotkey apps, remappers or the focused app can act on or
swallow them.
- Remappers: Karabiner-Elements remaps inside a virtual keyboard driver, so
keywatchshows Karabiner's output rather than the physical keys. - Timing: hold times and gaps come from when the tap receives each event. macOS stamps keyboard events and app-posted events in different units, so their own timestamps cannot be compared.
- Repeats: only the first repeat of a press gets a line. The up line counts them all.
keywatch is a key logger in the literal sense, so it is worth being explicit:
- It runs only while you run it, and nothing leaves your Mac.
- It writes to a file only when you pass
--out. That file contains everything you typed while it ran.
Requires the Xcode Command Line Tools and Mise.
mise install
mise run checkmise run buildbuilds.build/release/keywatch.mise run demo:recordrecords the README demo as you press keys, andmise run demorenders it toassets/demo/keywatch.gif.
Pushing a v* tag runs the release workflow. It builds arm64 and x86_64
archives, publishes the GitHub release, and updates the formula in
reemus-dev/homebrew-tap. The tap
update needs a HOMEBREW_TAP_DEPLOY_KEY secret that holds a deploy key with
write access to the tap.
