Hammerspoon, the dependency killer
2026-02-09
I bought a new MacBook recently, and in the process of setting it up, was frustrated by the number of customisation apps, tools, and licenses I had to configure to make my laptop feel like home: Karabiner for key remapping, Rectangle Pro for window management, Alfred for app launching/switching, and others. Many of these tools are incredibly powerful, but I only require a tiny subset of their capabilities. For that subset, I am encumbered by numerous dependencies that each have their own update cycle, launch daemons, may be occasionally broken by a macOS update, and (for the paid tools) may necessitate purchasing future versions down the line. Wouldn't it be nice to be able to have just my preferred subset of all of these tools all in a single place that's efficient, personalised, and version-controlled?
Enter Hammerspoon
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.
I used it to replicate everything I like about the aforementioned tools in ~140 lines of Lua. You can check out my dotfiles for the full configuration, but I'll explain what I did below.
Killing Karabiner
I used Karabiner(-Elements) to create a "hyper key", which is shorthand for Command + Option + Control + Shift. The idea is that 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.
Karabiner does this reliably but I've always been vaguely uncomfortable with how it works. If I understand correctly, 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. Out of all of the apps in my customisation stack it feels the most prone to breakage and is just 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: using it just to remap 2 keys feels like bringing a chainsaw to a knife fight.
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.
local hyper = hs.hotkey.modal.new()
hs.hotkey.bind({}, "f19",
function() hyper:enter() end,
function() hyper:exit() end
)
Since all of my hyper-related shortcuts live in Hammerspoon, this obviates the need for any further complex remapping.
Assassinating Alfred
Alfred is a great app, but I've found that I use it less and less. Firstly, Spotlight (and nothing else) has improved dramatically in macOS 26. All of the best Alfred workflows hook into Apple Shortcuts to work, but now, Shortcuts integrate best into Spotlight itself:

Secondly, each workflow is another dependency. Whilst default Alfred is rock-solid, many workflows are community maintained and some fall into disrepair over time. The Alfred Bear Search workflow still requires Apple Silicon Macs to install Rosetta; Spotlight supports searching in Bear directly.
More fundamentally, I am enjoying the "interact with the app without leaving your launch bar" paradigm less and less over time. It only makes sense to me if it's arduous to switch to the full app; if you have hotkeys to directly focus your apps in the first place, why not just go there and use the native shortcuts to perform whatever action you intend to do? Hyper-C followed by Command-N to create a new calendar event feels much smoother to me than wrangling with modals from within Alfred.
To this end, I realised that I only used Alfred for three things: shortcuts to toggle apps, toggling bluetooth device connectivity with the workflow atop, and text expansion. The last one was easy: macOS has built-in text replacements, and they sync with iPhone too! The other two require Hammerspoon.
App toggling
By this I mean pressing Hyper-[letter] to:
- Launch an app if it is quit
- Make it visible if it is hidden or buried under other windows
- Hide it if it is currently visible and active
This has become invaluable to me: I almost never use Command-Tab, Mission Control, or multiple desktops. I either use the hotkeys, or Spotlight/Alfred if the app is so infrequently used as to lack one.
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:
local appBindings = {
x = "com.apple.example",
...
}
local function toggleApp(bundleID)
local app = hs.application.get(bundleID)
if app and app:isFrontmost() then
app:hide()
else
hs.application.launchOrFocusByBundleID(bundleID)
end
end
for key, bundleID in pairs(appBindings) do
hyper:bind({}, key, function() toggleApp(bundleID) end)
end
You can find bundle IDs by running osascript -e 'id of app "AppName"' but it's more convenient to just give Claude the list of apps you want shortcuts for and let it do the grunt work.
Toggling Bluetooth devices
The Hammerspoon solution was a huge improvement over atop for me. I configured Hyper-1 to connect/disconnect my AirPods, and Hyper-2 for my Sony over-ears.
The actual connectivity is handled by blueutil, Hammerspoon's hs.alert handles notifications and hs.timer is used for a timeout condition (see dotfiles for full config).
Retiring Rectangle
Rectangle Pro 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. So why not just standard rectangle? I bought Pro for two reasons: the ability to cycle through specific sizes on repeated action, and cursor-follows-window when you send a window to another screen.
Turns out, both of these things are easily attainable in Hammerspoon. I've set up the following:
- Hyper-J: tile left half (tile left two-thirds on repetition)
- Hyper-K: maximise
- Hyper-L: tile right half (tile right two-thirds on repetition)
local sizes = { left = {0.5, 2/3}, right = {0.5, 1/3} }
-- cycle to next size on repeated press
lastIdx = (lastIdx % #sizes[direction]) + 1
win:moveToUnit({0, 0, sizes[direction][lastIdx], 1})
- Hyper-H sends to next screen and warps the cursor
win:moveToScreen(win:screen():next())
hs.mouse.absolutePosition(hs.geometry.rectMidPoint(win:frame()))
Why even bother?
None of these apps I replaced are particularly inefficient. Even if you only use them for one of their hundred features, you are hardly eating into your system resources in a noticeable way. Maybe there will be the occasional issue requiring a quit and reopen or waiting a bit before updating your OS, but all in all these apps are battle-tested. Moreover, small, tastefully opinionated, single-use Mac apps are an institution, and I would hate to imply that it's a waste to support the developers who make them.
But I don't think the point of customisation is efficiency. Perhaps there are 10x programmers for whom hotkeys and such genuinely save time, and for them tasteful defaults are definitely preferable. For me it is closer to interior design. I view my computer as a space I inhabit so naturally desire to make it feel bespoke, effortlessly navigable, an extension of my mind.
CLI coding agents make this view possible. As Hammerspoon is nothing more than a Lua bridge to the OS, Claude Code can script in it pretty fluently. The UX, then, is just talking to the computer: I tell it how I'd prefer things to work, and it will conform itself to my wishes. No matter how well-designed, how can any GUI drag-and-drop interface live up to the sheer boundlessness of this? This feels like how it should have always been.