DEV Community

AttractivePenguin
AttractivePenguin

Posted on

Hammerspoon: The macOS Automation Powerhouse You're Not Using (But Should Be)

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
Enter fullscreen mode Exit fullscreen mode

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!")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)