Open source / behind the scenes · · 6 min read

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

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:

  1. The character code (32 = space), or 65535 if there isn’t one
  2. The virtual key code (49 = kVK_Space)
  3. The modifier flags, and here they are standard NSEvent.ModifierFlags values: 1048576 is 1 << 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:

Putting it together: normalization and grouping

With all three sources read, HotkeyClash:

  1. 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.
  2. Groups by combo. Any combo with more than one claimant is a conflict.
  3. 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.
  4. 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.

Related reading