Skip to main content

Hammerspoon — Beginner's Guide

Overview

Hammerspoon is a free, open-source macOS automation tool that bridges the operating system's native APIs (windows, displays, Wi-Fi, audio, hotkeys, USB, filesystem events, and more) into a scriptable Lua environment. If AppleScript feels clunky and Shortcuts.app feels limiting, Hammerspoon is the power-user's answer: a few lines of Lua can rearrange every window on your screen, toggle your mute button on a meeting, re-map caps lock, or fire a notification when your external monitor connects.

By the end of this guide you will have Hammerspoon installed, understand its init.lua entry point, and have a working configuration with hotkeys for window snapping, a reload shortcut, and a simple event watcher. This guide pairs well with [[moom-beginner-guide|Moom]] and [[bunch-beginner-guide|Bunch]] — Hammerspoon is often the "glue" that wires those tools to events.

Prerequisites

  • macOS 12 (Monterey) or newer — macOS 15+ recommended
  • Administrator access on your Mac
  • Homebrew installed (optional but recommended)
  • Comfort editing a text file in your terminal or a GUI editor
  • Familiarity with basic macOS Accessibility permissions
  • Optional: familiarity with [[dotfiles-beginner-guide]] for versioning your config

You do not need prior Lua experience. You will pick up enough Lua from the examples below to be productive.

Key Concepts

hs global. Every Hammerspoon API is reachable through the hs namespace: hs.hotkey, hs.window, hs.screen, hs.wifi, hs.application, and so on.

~/.hammerspoon/init.lua. This is the single Lua file Hammerspoon executes at launch. Everything else — modules, spoons, helpers — is loaded from here.

Spoons. Reusable Lua modules with a .spoon extension. Think of them like plugins. Install into ~/.hammerspoon/Spoons/ and load with hs.loadSpoon("Name").

Accessibility API. Hammerspoon manipulates windows via macOS's Accessibility framework, the same mechanism used by [[moom-deep-dive|Moom]], [[yabai-beginner-guide|Yabai]], and similar tools. You must grant Hammerspoon Accessibility permission in System Settings.

Reload pattern. Changes to init.lua require a reload. You will bind a hotkey for this so you don't have to click the menu bar every time.

Step-by-Step Instructions

1. Install Hammerspoon

brew install --cask hammerspoon

Expected output (abridged):

==> Downloading https://github.com/Hammerspoon/hammerspoon/releases/...
🍺 hammerspoon was successfully installed!

Alternatively, download the .zip from hammerspoon.org and drag Hammerspoon.app to /Applications.

2. Launch Hammerspoon and grant permissions

Open Hammerspoon from /Applications or Spotlight. You'll see a new menu-bar icon. The app will prompt for Accessibility permission.

System Settings → Privacy & Security → Accessibility → toggle Hammerspoon ON

If you skip this, every window-manipulation call will fail silently.

3. Create your init.lua

mkdir -p ~/.hammerspoon
touch ~/.hammerspoon/init.lua
open -e ~/.hammerspoon/init.lua

4. Add a "Hello, Hammerspoon" and a reload hotkey

Paste this into init.lua:

-- ~/.hammerspoon/init.lua

-- A friendly confirmation that the config loaded
hs.alert.show("Hammerspoon config loaded", 1)

-- Reload config with ⌘⌥⌃ R
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "R", function()
hs.reload()
end)

Click the Hammerspoon menu-bar icon → Reload Config. You should see a semi-transparent banner appear briefly.

5. Add window snapping hotkeys

-- Move focused window to left half of screen
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "Left", function()
local win = hs.window.focusedWindow()
if not win then return end
local f = win:screen():frame()
win:setFrame({x = f.x, y = f.y, w = f.w / 2, h = f.h})
end)

-- Right half
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "Right", function()
local win = hs.window.focusedWindow()
if not win then return end
local f = win:screen():frame()
win:setFrame({x = f.x + f.w / 2, y = f.y, w = f.w / 2, h = f.h})
end)

-- Maximize
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "M", function()
local win = hs.window.focusedWindow()
if not win then return end
win:maximize()
end)

Press ⌘⌥⌃ ← with any window focused. It will jump to the left half of your screen.

6. Persist across logins

Hammerspoon has a "Launch at Login" option in its preferences dialog. Turn it on so your automations survive reboots.

Practical Examples

Example 1 — Auto-mute the system when headphones disconnect

local function onAudioDeviceChange()
local out = hs.audiodevice.defaultOutputDevice()
if out and not out:name():match("Head") then
hs.audiodevice.defaultOutputDevice():setMuted(true)
hs.alert.show("Speakers muted (headphones disconnected)")
end
end

hs.audiodevice.watcher.setCallback(onAudioDeviceChange)
hs.audiodevice.watcher.start()

Example 2 — Layout on Wi-Fi network change

Perfect for "when I connect to OfficeWiFi, move Slack to Space 2."

local wifiWatcher = hs.wifi.watcher.new(function()
local ssid = hs.wifi.currentNetwork()
if ssid == "OfficeWiFi" then
hs.application.launchOrFocus("Slack")
hs.alert.show("Office mode")
elseif ssid == "HomeWiFi" then
hs.application.launchOrFocus("Music")
hs.alert.show("Home mode")
end
end)
wifiWatcher:start()

Example 3 — App launcher hotkeys

local apps = {
c = "Google Chrome",
s = "Slack",
t = "Ghostty",
o = "Obsidian",
}

for key, app in pairs(apps) do
hs.hotkey.bind({"cmd", "alt", "ctrl"}, key, function()
hs.application.launchOrFocus(app)
end)
end

Example 4 — Caffeine replacement

local caffeine = hs.menubar.new()
local function setCaffeineDisplay(state)
caffeine:setTitle(state and "☕️" or "💤")
end
caffeine:setClickCallback(function()
setCaffeineDisplay(hs.caffeinate.toggle("displayIdle"))
end)
setCaffeineDisplay(hs.caffeinate.get("displayIdle"))

Hands-On Exercises

  1. Reload hotkey. Add a hotkey that reloads your config with a sound effect (hs.sound.getByName("Glass"):play()).
  2. Center a window. Write a hotkey that centers the focused window to 60% width × 70% height of the current screen.
  3. Move between screens. Bind ⌘⌥⌃ N to push the focused window to the next screen (hs.window.focusedWindow():moveToScreen(next_screen)).
  4. Battery notification. Write a watcher that shows a hs.notify banner whenever battery drops below 20%.
  5. Spoon install. Install the ReloadConfiguration spoon and replace your hand-rolled reload logic with it.
  6. File-change reload. Use hs.pathwatcher.new to auto-reload when init.lua changes on disk.

Troubleshooting

Nothing happens when I press a hotkey. Check the Hammerspoon console (menu-bar icon → Console). Most errors show there. The most common culprit is a missing Accessibility grant — revisit System Settings.

Two hotkeys conflict. Only one app can own a given keystroke. If a system shortcut or another app (Rectangle, Moom, Raycast) already binds ⌘⌥⌃ ←, Hammerspoon's bind will silently lose. Change one of them. See [[moom-deep-dive]] for advice on reserving the ⌘⌥⌃⇧ modifier strictly for Hammerspoon.

hs.window.focusedWindow() returns nil. You may be targeting a non-standard window (menu extras, Control Center). Guard with if not win then return end.

My config loaded but changes don't apply. You need to reload. Click the menu-bar icon → Reload Config, or press your reload hotkey.

Permission drift after a macOS update. After major macOS upgrades, Accessibility grants can silently reset. Re-toggle Hammerspoon in Privacy & Security.

References

  • [[hammerspoon-deep-dive]] — internals, event model, and advanced patterns
  • [[moom-beginner-guide|Moom Beginner Guide]] — pair Moom for layouts with Hammerspoon for triggers
  • [[moom-deep-dive]] — scripting Moom from Hammerspoon
  • [[bunch-beginner-guide|Bunch Beginner Guide]] — launch app bundles triggered by Hammerspoon hotkeys
  • [[macos-app-layout-beginner-guide]] — where Hammerspoon fits in the macOS automation stack
  • [[macos-app-layout-deep-dive]] — complete stack: Moom, Bunch, Shortcuts, Hammerspoon, launchd
  • [[yabai-beginner-guide|Yabai Beginner Guide]] — tiling window manager; often paired with Hammerspoon
  • [[dotfiles-beginner-guide]] — version your ~/.hammerspoon directory
  • [[chezmoi-beginner-guide]] — templated Hammerspoon configs across machines
  • [[karabiner-elements-beginner-guide|Karabiner-Elements Beginner Guide]] — remap keys and build Hyper/Meh layers that trigger Hammerspoon hotkeys
  • [[karabiner-elements-deep-dive|Karabiner-Elements Deep Dive]] — advanced keyboard remapping with shell_command integration for Hammerspoon

Summary

Hammerspoon turns macOS into a programmable workstation. With a dozen lines of Lua you can build window tilers, audio watchers, Wi-Fi-aware launchers, and app-specific keymaps that rival commercial apps. The mental model is simple: events come in, Lua callbacks run, macOS APIs are called.

Key takeaways:

  • One file — ~/.hammerspoon/init.lua — is your entire configuration
  • Bind a reload hotkey first; iterate fast
  • Accessibility permission is mandatory
  • Spoons package common patterns for reuse
  • Hammerspoon excels as the event layer in a stack with [[moom-beginner-guide|Moom]] (geometry) and [[bunch-beginner-guide|Bunch]] (app orchestration)

Next steps: read the [[hammerspoon-deep-dive]] for architectural details, install one or two Spoons from the gallery, and version-control your ~/.hammerspoon directory with [[chezmoi-beginner-guide|chezmoi]] or a plain dotfiles repo.