Manav B. Ponnekanti

Over-customising with Hammerspoon

22 February 2026; updated 12 March 2026

Hammerspoon is a free, open-source program that gives you deep access to macOS APIs via Lua scripting. In the Before Times, the barrier to entry would be your ability/willingness to write Lua, but now we have Claude Code as a non-deterministic compiler that turns English into functioning code (h/t yomismoaqui) so a quick read of the docs is sufficient to do whatever you'd like, even if you are only semi-technical like me.

It was incredibly satisfying to replace a few 3rd party dependencies that I kept around for a tiny subset of their features, as well as to create some bespoke solutions to very specific problems, all in ~170 lines of Lua. You can check out the config file and/or read the longer explanation below.

Hyper key

The "Hyper key" is the foundation for all of the rest of the shortcuts – it is a key that you remap to Command-Option-Control-Shift. Since it is such an unwieldy combination of modifiers, you can be assured that there will be no shortcut conflicts, allowing you to create keybindings for various things with abandon. I remap both my Caps Lock and Right Command key to Hyper as I use neither for their regular function but use the Hyper key a lot, so it's nice to have opposable chords for shortcuts instead of having to contort my hand.

In the past I used Karabiner-Elements for this. It uses a driver extension to intercept keystrokes at a very low level and passes them through a virtual keyboard, which is where your remapped keys lie. It works, but it's prone to breakage with macOS updates and is generally a bit janky – it doesn't play nice with the British ISO keyboard layout, does not uninstall cleanly, and has the occasional memory leak. It's also by far the most underutilised program on my computer. I'd prefer something more stable with a smaller scope, given that I'm just remapping a couple of keys.

Instead, I use macOS' native command-line remapping tool, hidutil (you can easily create remapping scripts here). This is extremely reliable and has virtually zero footprint, but the only problem is that it only allows one-to-one remapping, so remapping Caps/Right Command to multiple modifiers is out. Instead, I remap both to F19, and make Hammerspoon treat F19 as a "pseudo-Hyper" using hs.hotkey.modal. I also use a watcher to reset the modal state on on every wake, so as to not accidentally get stuck in a weird depressed modal state.

Since all of my hyper-related shortcuts live in Hammerspoon, this obviates the need for any further complex remapping. Another advantage of having your hyper key be a discrete key instead of a combo is that it unlocks some bonus ergonomic shortcuts when used with modifier keys, e.g. I have Command-Hyper set up to open my clipboard history in Alfred and Shift-Hyper to open Alfred Universal Actions.

App toggling

I find it irritating to wrangle with Command-Tab or the trackpad to switch between programs when I'm in flow. You either have to move away from the keyboard or tab your way through a long list of apps on the screen using visual search.

To solve this, I configured Hyper-[letter] hotkeys (e.g. Hyper-V launches VS Code) to:

This means I don't really need to do 'window management'. I can just call up and dismiss apps as and when I need them with near-zero switching cost.

Hammerspoon can launch and toggle apps by app name, path, or bundle ID. I took the bundle ID route, as it seemed the most robust. You can find bundle IDs by running osascript -e 'id of app "AppName"'.

Sometimes I want to reassign these shortcuts on the fly. For instance, I switch LLM providers and browsers often. Dealing with bundle IDs and editing the config file manually can be annoying in these instances, so I configured Hyper-` to enter an assign mode where I can associate a Hyper-[letter] key-bind with the focused app.

Window management

Before Hammerspoon, I used Rectangle Pro for this. It has all kinds of tiling features: custom layouts, trackpad gestures to move windows around, pinning and stashing windows, etc, but my windowing needs are very simple. I only ever use windows maximised, in a 50/50 split, or a two-thirds/one-third split. I just need two specific features: the ability to cycle through specific sizes on repeated action, and cursor-follows-window when you send a window to another screen.

Turns out that both of these things are easily attainable in Hammerspoon. I've set up the following:

This integrates nicely with the app toggling shortcuts, as it means I can launch an app and tile it with the same modifier held down.

Toggling Bluetooth devices

I've always found automatic bluetooth switching and multipoint to be unreliable, so turn both off and manually connect to my bluetooth devices. Sometimes it can be annoying to try to pair headphones through the menu bar icon; it's not very responsive, and they don't always show up in the list of available devices.

Hammerspoon doesn't have built-in capabilities for interfacing with Bluetooth, but it can hook into CLI tools. Using blueutil for the actual connectivity, I configured Hyper-1 to instantly pair/unpair my over-ear headphones.

Pomodoro timer

I am a big fan of David Cain's How To Do Things, which proposes a kind of simplified Pomodoro system wherein work is conceptualised in sets of uninterrupted 25-minute sessions he calls "blocks", each with a pre-meditated intent/finish line. The emphasis is on uninterrupted: any distraction or deviation invalidates the block. There is no system for breaks - they are ad-hoc and you don't need to always take them. I make no claims about the general efficacy of this way of working but it suits me well.

When I'm at home I just use a physical timer for this, but I don't particularly want to carry one around with me. A digital solution has to be dead simple: an unobtrusive but always-visible 25 minute timer with no option to pause, only to reset.

To solve this, I configured Hyper-2 to spawn a 25 minute countdown timer which displays in a translucent hs.canvas on the top-right of my screen. Repeating the shortcut while the timer is running just cancels it and displays the message 'Womp Womp: timer cancelled', which is sufficiently mild negative reinforcement for bailing. All I need to make some good blocks.

Reload hotkey

I used to use the ReloadConfiguration Spoon to automatically reload the configuration any time an edit is made to init.lua, but for reasons opaque to me, it would reload my config every time I opened VS Code, which was very annoying. Instead, I just bound a manual config reload to Command-Shift-R.