Hammerspoon: The macOS Automation Powerhouse You're Not Using (But Should Be)
If you've ever wished macOS had more powerful automation tools—something beyond the limited Automator and AppleScript—you're not alone. A recent Hacker News thread (176 points, 72 comments) brought Hammerspoon back into the spotlight, and for good reason. This open-source tool bridges Lua scripting with macOS system-level operations, giving developers unprecedented control over their workflow.
Why Hammerspoon Matters
macOS is a fantastic operating system, but its automation capabilities have always felt incomplete. Automator is limited to predefined actions. AppleScript works but has a steep learning curve and inconsistent application support. Shortcuts.App (introduced in Monterey) is consumer-focused and lacks developer-centric features.
Hammerspoon fills this gap. It exposes the entire macOS API through Lua, a lightweight scripting language that's easy to learn and fast to execute. With Hammerspoon, you can:
- Automate window management (move, resize, snap windows to positions)
- Create custom keyboard shortcuts for any application or system action
- Control applications programmatically (launch, quit, hide, show)
- React to system events (WiFi changes, USB device connections, screen wake)
- Build complex workflows that chain multiple actions together
- Create HTTP servers that respond to web requests
The best part? It's completely free and open-source, with an active community contributing extensions.
Getting Started with Hammerspoon
Installation
# Using Homebrew (recommended)
brew install --cask hammerspoon
# Or download from https://github.com/Hammerspoon/hammerspoon/releases
After installation, launch Hammerspoon from your Applications folder. macOS will ask for accessibility permissions—grant them, as Hammerspoon needs these to control windows and receive keyboard events.
Your First Script
Hammerspoon's configuration lives in ~/.hammerspoon/init.lua. Open it in your favorite editor:
-- Simple example: Reload Hammerspoon config with Cmd+Shift+R
hs.hotkey.bind({"cmd", "shift"}, "R", function()
hs.reload()
end)
-- A simple alert to confirm it's working
hs.alert.show("Hammerspoon config loaded!")
Save the file, then press Cmd+Shift+R in Hammerspoon to reload. You should see a small alert confirming the config loaded.
Practical Window Management
One of Hammerspoon's most popular uses is window management. Here's a practical setup:
-- Window layout definitions
local layouts = {
left = hs.layout.left50,
right = hs.layout.right50,
maximize = hs.layout.maximized,
top = hs.layout.top50,
bottom = hs.layout.bottom50
}
-- Function to move current window to a position
local function moveTo(position)
local win = hs.window.focusedWindow()
if win then
hs.layout.apply({{nil, win, nil, position}})
end
end
-- Keyboard shortcuts for window positioning
hs.hotkey.bind({"cmd", "alt"}, "Left", function() moveTo(layouts.left) end)
hs.hotkey.bind({"cmd", "alt"}, "Right", function() moveTo(layouts.right) end)
hs.hotkey.bind({"cmd", "alt"}, "Up", function() moveTo(layouts.maximize) end)
hs.hotkey.bind({"cmd", "alt"}, "Down", function() moveTo(layouts.bottom) end)
hs.hotkey.bind({"cmd", "alt"}, "M", function() moveTo(layouts.maximize) end)
Now Cmd+Alt+Left/Right/Up/Down snaps windows to common positions—no more manual resizing!
Application Launcher
Create a simple application launcher with fuzzy search:
-- Build a list of applications
local appList = {}
for _, app in ipairs(hs.application.runningApplications()) do
table.insert(appList, app:name())
end
-- Create a chooser for app selection
local chooser = hs.chooser.new(function(choice)
if choice then
hs.application.launchOrFocus(choice.text)
end
end)
chooser:choices(function()
local choices = {}
for _, app in ipairs(appList) do
table.insert(choices, {text = app})
end
return choices
end)
-- Bind to Cmd+Space (or your preferred shortcut)
hs.hotkey.bind({"cmd"}, "space", function()
chooser:show()
end)
Reacting to System Events
Hammerspoon can watch for system events and react automatically:
-- Auto-switch audio output when headphones are connected
hs.audiodevice.watcher.setCallback(function(event)
if event == "devicelistchanged" then
local defaultOutput = hs.audiodevice.defaultOutputDevice()
if defaultOutput:name():find("Headphones") then
hs.alert.show("Headphones connected")
-- Add any specific actions here
end
end
end)
hs.audiodevice.watcher.start()
-- React to WiFi network changes
hs.wifi.watcher.setCallback(function(event)
if event == "SSIDChanged" then
local ssid = hs.wifi.currentNetwork()
hs.alert.show("Connected to: " .. (ssid or "Unknown"))
-- Trigger actions based on network
if ssid == "Office-WiFi" then
hs.applescript('tell application "Slack" to launch')
end
end
end)
hs.wifi.watcher.start()
Creating a Hyper Key
One of Hammerspoon's most powerful features is creating a "hyper key"—a single key that acts as multiple modifiers:
-- Use Caps Lock as a hyper key (when held, acts as Cmd+Alt+Ctrl+Shift)
hs.loadSpoon("ShiftIt") -- If you have ShiftIt spoon installed
local hyper = hs.hotkey.modal.new({"cmd", "alt", "ctrl", "shift"})
-- Now any key press with Caps Lock held can trigger custom actions
hyper:bind({}, "C", function()
hs.alert.show("Hyper+C pressed!")
end)
hyper:bind({}, "E", function()
hs.application.launchOrFocus("Finder")
hs.application.launchOrFocus("Terminal")
end)
Real-World Scenarios
Scenario 1: Development Workspace Setup
local function setupDevelopmentWorkspace()
-- Launch terminal in left half
hs.application.launchOrFocus("Terminal")
local terminal = hs.application("Terminal")
hs.timer.doAfter(1, function()
hs.layout.apply({{nil, terminal:mainWindow(), nil, hs.layout.left50}})
end)
-- Launch editor in right half
hs.application.launchOrFocus("Visual Studio Code")
local vscode = hs.application("Visual Studio Code")
hs.timer.doAfter(2, function()
hs.layout.apply({{nil, vscode:mainWindow(), nil, hs.layout.right50}})
end)
end
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "D", setupDevelopmentWorkspace)
Scenario 2: Meeting Mode
local function meetingMode()
-- Mute audio
hs.audiodevice.defaultInputDevice():setMuted(true)
-- Pause music if playing
hs.osascript.applescript([[
tell application "Music"
if it is running then
pause
end if
end tell
]])
hs.alert.show("Meeting mode enabled")
end
local function endMeetingMode()
hs.audiodevice.defaultInputDevice():setMuted(false)
hs.alert.show("Meeting mode disabled")
end
hs.hotkey.bind({"cmd", "alt"}, "M", meetingMode)
hs.hotkey.bind({"cmd", "alt", "shift"}, "M", endMeetingMode)
Scenario 3: Clipboard History Manager
-- Simple clipboard history
local clipboardHistory = {}
local maxHistory = 10
hs.pasteboard.clearContents()
hs.hotkey.bind({"cmd", "alt"}, "V", function()
local menuData = {}
for i, item in ipairs(clipboardHistory) do
table.insert(menuData, {
title = item:sub(1, 50) .. (item:len() > 50 and "..." or ""),
fn = function()
hs.pasteboard.setContents(item)
hs.eventtap.keyStrokes("Cmd+V")
end
})
end
hs.menubar.new():setMenu(menuData):popupMenu(hs.mouse.getAbsolutePosition())
end)
-- Watch clipboard changes
hs.pasteboard.watcher.new(function(contents)
if contents and #contents > 0 then
table.insert(clipboardHistory, 1, contents)
if #clipboardHistory > maxHistory then
table.remove(clipboardHistory)
end
end
end):start()
FAQ and Troubleshooting
Q: Why isn't my hotkey working?
A: Check for conflicts with system shortcuts. Go to System Preferences > Keyboard > Shortcuts and look for overlaps. Also verify Hammerspoon has accessibility permissions in Security & Privacy settings.
Q: How do I debug my scripts?
A: Open the Hammerspoon Console (from the menu bar icon) to see error messages. Use hs.console.printStyledText() for debugging output. The console also shows stack traces for errors.
Q: My window layouts aren't applying correctly. Why?
A: Some applications take time to respond to window commands. Use hs.timer.doAfter() with delays for problematic apps:
hs.timer.doAfter(0.5, function()
-- Apply layout here
end)
Q: Can I use external Lua libraries?
A: Yes, but you need to install them in Hammerspoon's Lua path. Use hs.doc.hsdocs to view documentation. For complex needs, consider using hs.task to run shell commands.
Q: How do I reload my config without restarting?
A: Use hs.reload() or create a hotkey:
hs.hotkey.bind({"cmd", "shift"}, "R", hs.reload)
Q: Where can I find more extensions?
A: The Hammerspoon Spoons repository has pre-built modules. Browse at https://github.com/Hammerspoon/spoons
Conclusion
Hammerspoon represents what macOS automation should have been: powerful, flexible, and accessible to developers. Whether you're a power user tired of manual window management or a developer building complex workflows, Hammerspoon offers the control you've been looking for.
The combination of Lua's simplicity with macOS's rich API surface creates something genuinely useful—automation that adapts to your workflow rather than forcing you to adapt to it. The growing community and extensive documentation make it approachable even for those new to scripting.
If you haven't tried Hammerspoon yet, give it an hour of your time. Start small—window management is the gateway drug—but don't be surprised if you find yourself automating more and more of your daily routine. Your future self will thank you.
Resources:
Top comments (0)