macOS 14 and later  ·  menu bar app  ·  one config file

True hotkeys for macOS.Nothing less, nothing more.

One plain‑text file. Any key becomes a command: a shell script, Python, AppleScript, a menu item, a keystroke. And it fires only when the app or window you named is in front.

brew install --cask hotkeys-app/tap/hotkeys

hotkeys.ymlloaded

      

Config as code

One YAML file in git. Diff it, sync it with your dotfiles, copy a line from a colleague.

Context aware

The same key does a different job in every app and window, with an else for everywhere else.

Runs anything

Shell in your login shell, Python, AppleScript, a menu item by name, a keystroke into any app.

Native speed

A Swift event tap. The key is handled before the app in front even sees it. No Electron, no polling.

How it worksNo macro editor. No drag and drop. A text file you already know how to edit.

A key, a colon, a command.

Everything else is optional. Add a context when the same key should do different things in different apps. Save, reload, press.

01

Write a line

Modifiers are one symbol each. ^ control, ! option, * shift, # command.

^f1: runApp("AppCode")
_#l: runApp("Telegram")
f4: osascript -e 'display dialog "Hi"'
02

Add a context

By bundle id, by window title, by process. With an else when nothing matches.

f5:
  currentAppChrome():
    openMenuItem("Google Chrome",
      "View", "Reload This Page")
  else: runApp("Google Chrome")
03

Save, reload

Reload config from the menu, or bind reloadConfig() to a key. Macros keep long paths out of the way.

macros:
  ADB: ~/android/platform-tools/adb
hotkeys:
  f2:
    currentWindowAndroidEmulator:
      ADB shell input keyevent 187

Everything a hotkey tool should do. In one file.

Built for people who live in a terminal, an IDE and a browser and want the keyboard to do the walking.

Config as code

YAML with comments and macros. Lives in Application Support, so it goes into git and travels with your dotfiles. Reload from the menu or from a hotkey. No settings UI, ever.

Context aware

currentApp("com.apple.dt.Xcode"), currentWindowContains("Emulator"), currentProcessEquals("mc"). The same key, a different job in every app, and an else branch for the rest.

Run anything

Shell commands run in your login shell, so your PATH, brew and virtualenvs just work. AppleScript inline or as a block. open an app, a file, a URL.

Send keystrokes

sendKeys("control-comma"). Give a Mac app the shortcut it never had, remap Home and End per app, chain keys after a script.

Menu items by name

openMenuItem("Google Chrome", "View", "Reload This Page"). Any menu, any app, no screen coordinates, no recording of clicks.

Modifier‑only hotkeys

alone switches the keyboard layout, the way it does on Windows. Any combination of modifiers, no letter required.

Forward or consume

forward: true runs your action and still lets the key through to the app. forward: false swallows it. Per hotkey, no surprises.

Keyboard layouts

switchKeyboardLayout(us, ru) flips between any two input sources. displayAvailableKeyboardInputSources() lists their ids so you never guess.

Developer contexts

Android Emulator, iOS Simulator, Xcode, JetBrains, Chrome, Terminal, mc recognised out of the box. displayActiveWindowInfo() shows any app's bundle id and window title.

Native and light

Swift and a CGEvent tap, not Electron. The key is handled before the app in front even sees it. No polling loop, no dock icon, nothing to notice until you press a key.

Private

No account, no telemetry, no cloud. Your config never leaves the Mac. The only network call is validating your license key.

Free to start

Ten hotkeys free, no clock. A license unlocks the rest on three Macs, and updates arrive in‑app or through Homebrew.

ExamplesReal lines from real configs. Copy, paste, change the app name.

What people actually put in the file.

Four corners of one config. The full reference lives in the docs.

hotkeys:
  _#2: runApp("Xcode")
  _#3: runApp("AppCode")
  _#p: runApp("PyCharm")

  # previous and next tab, the way every other IDE does it
  ^!left:
    currentApp("com.apple.dt.Xcode"): sendKeys("control-comma")
  ^!right:
    currentApp("com.apple.dt.Xcode"): sendKeys("control-dot")

  # which app is this, what is its bundle id, what is the window called
  ^!#\: displayActiveWindowInfo()

Put the IDEs on 2, 3, P and stop hunting the dock.

hotkeys:
  f5:
    currentAppChrome(): openMenuItem("Google Chrome", "View", "Reload This Page")

  # Home and End that behave like everywhere else, only in the browser
  home:
    currentAppChrome(): sendKeys("command-up")
  end:
    currentAppChrome(): sendKeys("command-down")

  _#l: runApp("Telegram")
  _#semicolon: runApp("Discord")
  ^#o: runApp("System Settings")

Keys without a matching context pass straight through, so Home keeps doing its normal thing in every other app.

macros:
  ADB: ~/Library/Android/sdk/platform-tools/adb

hotkeys:
  # hardware keys for the emulator, no mouse trip to the side bar
  esc:
    currentWindowAndroidEmulator: ADB -e shell input keyevent KEYCODE_BACK
  f1:
    currentWindowAndroidEmulator: ADB -e shell input keyevent KEYCODE_HOME
  f2:
    currentWindowAndroidEmulator: ADB -e shell input keyevent KEYCODE_APP_SWITCH

  # the simulator gets its own branch of the same key
  _#esc:
    currentAppSimulatorIos(): sendKeys("command-shift-h")
    else: runApp("Android Studio")

Context by window title needs Screen Recording, macOS hides titles otherwise. Hotkeys asks once, only if your config uses it.

hotkeys:
  # shell, in your login shell, so ~/bin and the venv are on PATH
  ^!#t: ~/bin/timesheet.py start
  _#f11: ~/bin/textshot.sh

  # AppleScript, one line or a block
  f4: osascript -e 'display dialog "Hello world"'
  f9:
    currentApp("com.apple.dt.Xcode"):
      appleScript: |
        tell application id "com.jetbrains.fleet"
          activate
        end tell

  # the file that edits itself
  f10: openConfig()
  _#f10: reloadConfig()

Anything that works in Terminal works here. Same shell, same PATH, same profile.

Free for ten hotkeys. Cheap for all of them.

A license key unlocks everything on up to three Macs. No account, the key arrives by email.

Free

$0
no time limit
  • 10 hotkeys
  • Shell commands and AppleScript
  • Edit and reload from the menu bar
  • Built‑in methods and contexts
Download
the trial that never expires

Yearly

$9/ year
cancel any time
  • Unlimited hotkeys
  • Every built‑in method and context
  • sendKeys, openMenuItem, modifier‑only keys
  • 3 Macs, move the key whenever you like
  • All updates while active
Buy yearly
license key by email in a minute

Lifetime

$15once
yours for good
  • Everything in Yearly
  • Every future version, no renewal
  • 3 Macs
  • Pays for itself against yearly in under two years
Buy lifetime
license key by email in a minute

Checkout, invoices and tax are handled by Polar as merchant of record, VAT is included where it applies. Pick Yearly or Lifetime on the checkout page.

DownloadNotarized by Apple. Updates itself, or let Homebrew do it.

Get Hotkeys.

Hotkeys for Mac

macOS 14 Sonoma or later. Open the dmg, drag to Applications, grant Accessibility when asked.

Download dmg
# or
brew install --cask hotkeys-app/tap/hotkeys
version 20260921 · release feed

Updates and requirements

Hotkeys checks for updates on launch and installs them in place. Homebrew users can also run brew upgrade --cask hotkeys.

It needs the Accessibility permission, that is what lets it see and send keystrokes. Screen Recording is asked only if your config uses window‑title contexts.

Useful scripts source and issues on GitHub.

Download Hotkeys Lite on the App Store

Hotkeys Lite is the App Store edition. Sandboxed and permission‑free, it fires apps, files and scripts from a key. Sending keystrokes, opening menu items, mouse actions and modifier‑only hotkeys need the app above.

QuestionsEverything else is in the docs or one email away.
Why does it need Accessibility?

That permission is what lets an app see keystrokes meant for other apps and send its own. Every hotkey tool asks for it. Screen Recording is asked for separately and only if your config uses a window‑title context, because macOS hides other apps' window titles without it.

Can one key do a different job in every app?

That is what contexts are for. A context is a built‑in method that answers yes or no about the app in front. Put it under the key and the branch below it runs only when the answer is yes, with else for everywhere else.

_#esc:
  currentApp("com.apple.dt.Xcode"): sendKeys("command-shift-o")
  currentWindowAndroidEmulator: ADB -e shell input keyevent KEYCODE_HOME
  else: runApp("Android Studio")
I edited the config, how do I load it?

Hotkeys reads the file when it starts and when you choose Reload config in the menu bar. Saving alone changes nothing, there is no file watcher. Give both a key and you never touch the menu again: f10: openConfig() to open it, _#f10: reloadConfig() to reload it.

How do I find out what an app is called?

Bind displayActiveWindowInfo() to a key and press it while the app is in front. The dialog shows the bundle id, the process name and the window title, which are exactly the strings a context takes, as in currentApp("com.apple.dt.Xcode").

Does the app in front still receive the key?

No, a matched hotkey is consumed and the app never sees it. If the key has contexts and none of them match, it passes through untouched. forward overrides both, so you can let the key through after your action, or swallow it even when nothing matched.

Does it phone home?

No accounts, no analytics, no crash reporters. Hotkeys validates your license key with Polar once on activation and about once a week afterwards, and keeps working for thirty days offline.

Can I move my license to another Mac?

A key activates on three Macs. Choose Deactivate in the License menu on the old one and the slot is free again. Lost the email? Your keys and invoices are in the customer portal.

Where is the config file?

~/Library/Application Support/hotkeys/hotkeys.yml. The Edit config menu item opens it, and a commented template is written on first run.

Is there a trial?

The free tier is the trial: ten hotkeys, every action type, no clock. Buy when you run out of keys.

Something is broken, or I want a refund.

Write to [email protected] or open an issue on GitHub. Most problems are a one‑line config fix and get answered the same day. Refunds go through Polar, just ask.

Nothing less, nothing more.

Ten hotkeys free, the rest for the price of a coffee.