How to Read Every App's Keyboard Shortcuts on macOS with the Accessibility API
macOS has no public API that lists the keyboard shortcuts in use. Here's how HotkeyClash builds that list anyway: walking menu trees with AXUIElement, decoding the menu modifier bitmask, and parsing the symbolic hotkeys plist.
The first question most people ask about HotkeyClash is “how does it know?” macOS doesn’t offer a “list every keyboard shortcut” API. System Settings shows its own shortcuts, and each app shows its own, but nothing gives you the combined view.
So HotkeyClash builds that list from three sources that are readable. This post explains how each one works, including the odd parts, in case you want to build something similar or just want to know what a tool with Accessibility access is doing on your Mac. The snippets below are simplified illustrations of the technique. The real code is in the open-source repo.
What you can’t read: other apps’ global hotkeys
Start with the gap. Most launcher and utility hotkeys are registered with Carbon’s RegisterEventHotKey or read through an event tap. There is no public API to list hotkeys that other processes have registered. The system knows them, but it doesn’t tell anyone.
That’s why a tool like this has to work from the outside in: read everything that is exposed, and add config-file parsers for tools whose bindings live on disk. It’s also why the HotkeyClash roadmap includes parsers for Keyboard Maestro, BetterTouchTool, and Hammerspoon. Each parser adds a source that the OS itself won’t report.
Source 1: Menu shortcuts through the Accessibility API
Every menu item in every running app is exposed through the Accessibility API as an AXUIElement, including its keyboard shortcut. The walk looks like this:
import ApplicationServices
// 1. Get an accessibility handle for a running app
let app = AXUIElementCreateApplication(pid)
// 2. Get its menu bar
var menuBar: CFTypeRef?
AXUIElementCopyAttributeValue(app, kAXMenuBarAttribute as CFString, &menuBar)
// 3. Recurse through kAXChildrenAttribute:
// menu bar → menu bar items → menus → menu items → submenus …
For each menu item, the shortcut is split across a few attributes:
| Attribute | Contains |
|---|---|
kAXMenuItemCmdCharAttribute |
The key as a character, e.g. "N" |
kAXMenuItemCmdVirtualKeyAttribute |
A virtual key code, for keys without a character |
kAXMenuItemCmdGlyphAttribute |
A glyph ID for special keys (arrows, Delete, F-keys…) |
kAXMenuItemCmdModifiersAttribute |
The modifiers, as a bitmask (see below) |
kAXTitleAttribute |
The menu item’s name, e.g. “New Window” |
The modifier bitmask quirk
kAXMenuItemCmdModifiers doesn’t use the NSEvent.ModifierFlags values you’d expect. It uses a small legacy mask from the Carbon Menu Manager:
| Bit | Value | Meaning |
|---|---|---|
| — | 0 | ⌘ only |
| 0 | 1 | Shift |
| 1 | 2 | Option |
| 2 | 4 | Control |
| 3 | 8 | No Command |
Command is implied. It’s included unless bit 3 says otherwise. So 0 means ⌘, 1 means ⌘⇧, 12 (Control + No Command) means ⌃ alone, and 6 means ⌘⌥⌃. If you forget about the inverted Command bit, every menu shortcut you read comes out wrong in a way that looks almost right.
func decode(_ mask: Int) -> [String] {
var mods: [String] = []
if mask & 4 != 0 { mods.append("⌃") }
if mask & 2 != 0 { mods.append("⌥") }
if mask & 1 != 0 { mods.append("⇧") }
if mask & 8 == 0 { mods.append("⌘") } // Command unless "no command" is set
return mods
}
Practical problems
- Permission. None of this works until the user grants Accessibility access (
AXIsProcessTrustedWithOptionscan show the prompt). Without it, HotkeyClash skips running apps and still scans the other two sources. - Slow or unresponsive apps. Accessibility calls are IPC to the target process, so a hung app can stall the walk. Timeouts per app (
AXUIElementSetMessagingTimeout) keep one bad app from blocking the whole scan. - Dynamic menus. Some apps build menus lazily or change them based on state, so a scan reflects the menus at that moment.
- Cost. A big app like Xcode has thousands of menu items. Even so, a full scan across a typical set of running apps takes about three seconds.
Source 2: macOS system shortcuts from the symbolic hotkeys plist
macOS’s own shortcuts (Spotlight, Mission Control, screenshots, input sources) are stored per user in the com.apple.symbolichotkeys preferences domain, under AppleSymbolicHotKeys. Each entry is keyed by a numeric ID:
64 = {
enabled = 1;
value = {
parameters = (32, 49, 1048576);
type = standard;
};
};
The three parameters are:
- The character code (
32= space), or65535if there isn’t one - The virtual key code (
49= kVK_Space) - The modifier flags, and here they are standard
NSEvent.ModifierFlagsvalues:1048576is1 << 20, Command
So ID 64 is “Show Spotlight search” on ⌘Space. ID 60 is “Select the previous input source”, usually (32, 49, 262144): ⌃Space, since 262144 is 1 << 18, Control.
The awkward part: the IDs aren’t documented anywhere official, and entries you’ve never customized often don’t appear in the plist, because macOS falls back to built-in defaults. A useful reader needs a table that maps IDs to names and defaults, and it needs to respect enabled = 0. A disabled system shortcut isn’t a conflict, and treating it as one would fill the results with false alarms.
Source 3: Config files
For tools that keep their bindings on disk, parsing is simpler and more reliable than anything the OS provides:
- Karabiner-Elements:
~/.config/karabiner/karabiner.json. Complex modification rules havefromblocks with akey_codeandmodifiers.mandatory, which is exactly the combo the rule takes. - skhd:
~/.config/skhd/skhdrc. It’s line-based:cmd + shift - k : command, withhyperandmehas shorthand for four and three modifiers.
Putting it together: normalization and grouping
With all three sources read, HotkeyClash:
- Normalizes modifiers. It drops Caps Lock and device-specific flags (left vs. right Command, for example) so that combos which mean the same thing compare as equal, and it expands
hyper/meh. - Groups by combo. Any combo with more than one claimant is a conflict.
- Classifies severity. Two global claimants (system shortcut, config file) on one combo is a red definite conflict. A global claimant overlapping an app’s menu shortcut is an amber potential conflict, because it only affects you while that app is frontmost.
- Sorts by severity, then by how simple the combo is, since a clash on ⌘K hurts more than one on ⌃⌥⇧F7.
Why it’s open source
A tool that reads other apps’ UI through Accessibility is a tool you should be able to audit. That permission could in principle read far more than menu titles. HotkeyClash makes zero network requests, keeps scan results in memory only, and the code that touches AXUIElement is public under GPL-2.0, so you can check that yourself.
If you want to add a parser (Keyboard Maestro, BetterTouchTool, Hammerspoon, Alfred, and Raycast configs are all on the roadmap), each parser is a self-contained module, and contributions are welcome on GitHub. To try it first, download HotkeyClash. It’s free and runs on macOS 14+.
Frequently asked questions
Is there a macOS API to list all registered keyboard shortcuts?
No. There's no public API that returns global hotkeys registered by other apps (for example through Carbon's RegisterEventHotKey). What you can read is each running app's menu shortcuts through the Accessibility API, macOS's own shortcuts from the com.apple.symbolichotkeys preferences, and the config files of tools like Karabiner-Elements and skhd.
Why does reading menu shortcuts need Accessibility permission?
Other apps' menu bars are only visible through AXUIElement, and macOS requires the user to grant Accessibility access before one process can inspect another's UI. HotkeyClash asks for it only to read menu shortcuts, and because it's GPL-2.0, the code that uses the permission is public.
What does kAXMenuItemCmdModifiers return?
A small bitmask, not NSEvent modifier flags. Shift is 1, Option is 2, Control is 4, and 8 means 'no Command'. Command is implied unless bit 8 is set, so a value of 0 means ⌘ alone.
Is HotkeyClash's source code available?
Yes. HotkeyClash is open source under GPL-2.0 on GitHub. It's written in Swift 6 with SwiftUI and AppKit, about 2,300 lines.