Hammerspoon macOS Automation
Hammerspoon bridges macOS and Lua scripting for powerful desktop automation.
Directory Structure
~/.hammerspoon/ ├── init.lua # Main entry point (always loaded on startup) ├── Spoons/ # Plugin directory │ └── *.spoon/ # Individual Spoon packages │ └── init.lua # Spoon entry point └── .gitignore
Configuration Basics
init.lua - Entry Point
Hammerspoon always loads ~/.hammerspoon/init.lua on startup:
-- Enable CLI support (required for hs command) require("hs.ipc")
-- Load a Spoon hs.loadSpoon("SpoonName")
-- Configure the Spoon spoon.SpoonName:bindHotkeys({...})
Loading Spoons
-- Load and auto-init (default) hs.loadSpoon("MySpoon")
-- Load without global namespace local mySpoon = hs.loadSpoon("MySpoon", false)
When loaded, Spoons are accessible via spoon.SpoonName .
CLI Usage (hs command)
Prerequisite: Add require("hs.ipc") to init.lua, then reload manually once.
Reload configuration
hs -c 'hs.reload()'
Show alert on screen
hs -c 'hs.alert("Hello from CLI")'
Run any Lua code
hs -c 'print(hs.host.locale.current())'
Get focused window info
hs -c 'print(hs.window.focusedWindow():title())'
Window Management with ShiftIt
ShiftIt is a popular Spoon for window tiling.
Installation
Download from https://github.com/peterklijn/hammerspoon-shiftit
Extract to ~/.hammerspoon/Spoons/ShiftIt.spoon/
Configuration
require("hs.ipc") hs.loadSpoon("ShiftIt")
spoon.ShiftIt:bindHotkeys({ -- Halves left = { { 'ctrl', 'cmd' }, 'left' }, right = { { 'ctrl', 'cmd' }, 'right' }, up = { { 'ctrl', 'cmd' }, 'up' }, down = { { 'ctrl', 'cmd' }, 'down' },
-- Quarters
upleft = { { 'ctrl', 'cmd' }, '1' },
upright = { { 'ctrl', 'cmd' }, '2' },
botleft = { { 'ctrl', 'cmd' }, '3' },
botright = { { 'ctrl', 'cmd' }, '4' },
-- Other
maximum = { { 'ctrl', 'cmd' }, 'm' },
toggleFullScreen = { { 'ctrl', 'cmd' }, 'f' },
center = { { 'ctrl', 'cmd' }, 'c' },
nextScreen = { { 'ctrl', 'cmd' }, 'n' },
previousScreen = { { 'ctrl', 'cmd' }, 'p' },
resizeOut = { { 'ctrl', 'cmd' }, '=' },
resizeIn = { { 'ctrl', 'cmd' }, '-' },
})
Modifier Keys
Key Lua Name
Command 'cmd'
Control 'ctrl'
Option/Alt 'alt'
Shift 'shift'
Hotkey Binding (Without Spoons)
-- Simple hotkey hs.hotkey.bind({'cmd', 'alt'}, 'R', function() hs.reload() end)
-- Hotkey with message hs.hotkey.bind({'cmd', 'shift'}, 'H', function() hs.alert.show('Hello!') end)
Common Modules
hs.window - Window Management
-- Get focused window local win = hs.window.focusedWindow()
-- Move/resize win:moveToUnit('[0,0,0.5,1]') -- Left half win:maximize() win:centerOnScreen()
-- Get all windows local allWindows = hs.window.allWindows()
hs.application - App Control
-- Launch or focus app hs.application.launchOrFocus('Safari')
-- Get running app local app = hs.application.get('Finder') app:activate()
hs.alert - On-screen Messages
hs.alert.show('Message') hs.alert.show('Message', nil, nil, 3) -- 3 second duration
hs.notify - System Notifications
hs.notify.new({title='Title', informativeText='Body'}):send()
hs.caffeinate - Sleep/Wake
-- Prevent sleep hs.caffeinate.set('displayIdle', true)
-- Watch for sleep/wake events hs.caffeinate.watcher.new(function(event) if event == hs.caffeinate.watcher.systemWillSleep then print('Going to sleep') end end):start()
Spoons
What is a Spoon?
Self-contained Lua plugin with standard structure:
MySpoon.spoon/ └── init.lua # Required: exports a table with methods
Official Spoon Repository
SpoonInstall - Package Manager
hs.loadSpoon("SpoonInstall")
-- Install from official repo spoon.SpoonInstall:andUse("ReloadConfiguration", { start = true })
-- Install from custom repo spoon.SpoonInstall.repos.Custom = { url = "https://github.com/user/repo", desc = "Custom spoons", branch = "main", } spoon.SpoonInstall:andUse("CustomSpoon", { repo = "Custom" })
Configuration Reloading
Manual Reload
-
Click menubar icon -> "Reload Config"
-
Or bind a hotkey:
hs.hotkey.bind({'cmd', 'alt', 'ctrl'}, 'R', function() hs.reload() end)
Auto-reload on File Change
hs.loadSpoon("ReloadConfiguration") spoon.ReloadConfiguration:start()
Or manually:
local configWatcher = hs.pathwatcher.new(os.getenv('HOME') .. '/.hammerspoon/', function(files) for _, file in pairs(files) do if file:sub(-4) == '.lua' then hs.reload() return end end end):start()
CLI Reload
hs -c 'hs.reload()'
Note: Requires require("hs.ipc") in init.lua.
Troubleshooting
IPC Not Working
error: can't access Hammerspoon message port
Fix: Add require("hs.ipc") to init.lua and reload manually via menubar.
Spoon Not Loading
-
Check path: ~/.hammerspoon/Spoons/Name.spoon/init.lua
-
Check Lua syntax in Spoon's init.lua
-
Check Hammerspoon console for errors (menubar -> Console)
Hotkey Not Working
-
Check for conflicts with system shortcuts
-
Verify modifier key names are lowercase strings
-
Check console for binding errors
Console and Debugging
-- Print to console print('Debug message')
-- Inspect objects hs.inspect(someTable)
-- Open console hs.openConsole()
Access console: Menubar icon -> Console (or Cmd+Alt+C if bound)
Best Practices
-
Always use IPC - Add require("hs.ipc") for CLI support
-
Use Spoons - Don't reinvent window management
-
Version control - Track ~/.hammerspoon/ with git
-
Capture variables - Objects not stored in variables get garbage collected
-
Check console - First place to look for errors
References
-
Official Documentation
-
Getting Started Guide
-
Spoons Repository
-
GitHub Repository