TIL: Hammerspoon as a Karabiner-Elements replacement for Hyper/Meh keys
Karabiner-Elements suddenly stopped working on my MacBook. I rely on the Hyper key (⌃⌥⇧⌘) and Meh key (⌃⇧⌘) all day in Keyboard Maestro, mapped from right-Cmd and right-Option. Rather than fight the kernel extension, I rewrote the remap in Hammerspoon — pure Lua, no extension required.
The trick: swallow flagsChanged events for the physical right-Cmd / right-Option keys, track their state, and stamp the corresponding modifier combo onto every subsequent keyDown/keyUp. The device-specific bits (NX_DEVICERCMDKEYMASK = 0x10, NX_DEVICERALTKEYMASK = 0x40) let me tell which physical key fired, since macOS otherwise treats left and right modifiers as identical.
~/.hammerspoon/init.lua:
-- ~/.hammerspoon/init.lua
-- Right Cmd -> Hyper (Ctrl+Alt+Shift+Cmd)
-- Right Option -> Ctrl+Shift+Cmd
--
-- Approach: swallow the physical right-Cmd / right-Option flagsChanged events
-- and, while either is "held", stamp the corresponding modifier combo onto
-- every keyDown/keyUp that fires.
local KC_RCMD = 54
local KC_ROPT = 61
-- Device-specific CGEventFlag bits identifying which physical key fired.
-- (NX_DEVICERCMDKEYMASK = 0x10, NX_DEVICERALTKEYMASK = 0x40)
local RAW_RCMD = 0x10
local RAW_ROPT = 0x40
local state = {rcmd = false, ropt = false}
local et = hs.eventtap.event.types
local function stampFlags(event)
if state.rcmd and state.ropt then
event:setFlags({cmd = true, alt = true, shift = true})
elseif state.rcmd then
event:setFlags({cmd = true, alt = true, shift = true, ctrl = true})
elseif state.ropt then
event:setFlags({cmd = true, shift = true, ctrl = true})
end
end
local watcher = hs.eventtap.new(
{et.flagsChanged, et.keyDown, et.keyUp},
function(event)
if event:getType() == et.flagsChanged then
local kc = event:getKeyCode()
if kc == KC_RCMD then
state.rcmd = (event:rawFlags() & RAW_RCMD) ~= 0
return true, {}
elseif kc == KC_ROPT then
state.ropt = (event:rawFlags() & RAW_ROPT) ~= 0
return true, {}
end
end
stampFlags(event)
return false
end
)
watcher:start()
hs.alert.show("hammerspoon: right-modifier remaps active")
-- Diagnostic probes (delete once verified). Press the listed combo + K:
hs.hotkey.bind({"cmd","alt","shift","ctrl"}, "k", function() hs.alert.show("HYPER+K (rcmd alone)") end)
hs.hotkey.bind({"cmd","shift","ctrl"}, "k", function() hs.alert.show("MEH+K (ropt alone)") end)
hs.hotkey.bind({"cmd","alt","shift"}, "k", function() hs.alert.show("CHORD+K (rcmd+ropt)") end)
hs.hotkey.bind({"cmd","alt"}, "k", function() hs.alert.show("cmd+alt+K (no remap)") end)
Gotcha: Keyboard Maestro recognizes the Hyper and Meh keys natively when they come from Karabiner-Elements — there are dedicated trigger options for them. With Hammerspoon, KM sees only the raw modifier chord. So when defining a Hyper+K hotkey in KM, I have to bind it as ⌃⌥⇧⌘+K explicitly, not pick “Hyper” from the dropdown. Same for Meh → ⌃⇧⌘.
Reference: Hammerspoon eventtap docs.
Let's stay connected!
Author
I'm Oliver Nguyen. A software maker working mostly in Go and JavaScript. I enjoy learning and seeing a better version of myself each day. Occasionally spin off new open source projects. Share knowledge and thoughts during my journey.