| name | dev-test-hammerspoon |
| description | This skill should be used when the user asks to "debug macOS app", "test native app", "automate macOS workflow", "test native macOS application", or needs desktop automation for testing macOS applications with Hammerspoon. Use for application launch/control, window management, keyboard/mouse simulation, and visual verification. |
| user-invocable | false |
| disable-model-invocation | true |
Announce: "I'm using dev-test-hammerspoon for macOS desktop automation."
## Gate Reminder
Before taking screenshots or running E2E tests, you MUST complete all 6 gates from dev-tdd:
GATE 1: BUILD
GATE 2: LAUNCH (with file-based logging)
GATE 3: WAIT
GATE 4: CHECK PROCESS
GATE 5: READ LOGS ← MANDATORY, CANNOT SKIP
GATE 6: VERIFY LOGS
THEN: E2E tests/screenshots
You loaded dev-tdd earlier. Follow the gates now.
Contents
macOS Desktop Automation
## Tool Availability Gate
Verify Hammerspoon is installed before proceeding.
which hs || echo "MISSING: hs CLI"
ls /Applications/Hammerspoon.app 2>/dev/null || echo "MISSING: Hammerspoon.app"
If missing:
STOP: Cannot proceed with macOS automation.
Missing tool: Hammerspoon (required for macOS E2E testing)
Install with:
# Install via nix-darwin, or verify: hammerspoon -c "print('ok')"
After installing:
1. Open Hammerspoon.app
2. Grant Accessibility permissions in System Preferences
3. In Hammerspoon console, run: hs.ipc.cliInstall()
4. Add to ~/.hammerspoon/init.lua: require("hs.ipc")
Reply when installed and I'll continue testing.
This gate is non-negotiable. Missing tools = full stop.
## When to Use Hammerspoon
Use Hammerspoon for:
- macOS native application automation
- System-wide keyboard shortcuts
- Window management and positioning
- Menu item automation
- Clipboard verification
- Multi-app workflows on macOS
Do not use Hammerspoon for:
- Testing web applications (use Chrome MCP or Playwright)
- Cross-platform testing needed
- Linux desktop automation (use dev-test-linux)
For web testing, discover and read the relevant skill:
Related skills:
- Read
${CLAUDE_SKILL_DIR}/../../skills/dev-test-chrome/SKILL.md and follow its instructions.
- Read
${CLAUDE_SKILL_DIR}/../../skills/dev-test-playwright/SKILL.md and follow its instructions.
- Chrome MCP skill - debugging
- Playwright skill - CI/CD
Hammerspoon Facts
- Hammerspoon is more reliable than AppleScript for macOS automation, and web-testing tools (Chrome MCP, Playwright) cannot drive native macOS apps at all — substituting either leaves the native UI untested.
- Accessibility permissions are a one-time setup; avoiding the setup by testing manually produces no automated evidence, so "it worked" is an unverified claim.
Hammerspoon Setup
One-time setup in ~/.hammerspoon/init.lua:
require("hs.ipc")
Reload config after changes:
hs -c 'hs.reload()'
Input Simulation
hs.eventtap - Keyboard/Mouse
hs.eventtap.keyStrokes("hello world")
hs.eventtap.keyStroke({"cmd"}, "c")
hs.eventtap.keyStroke({"cmd", "shift"}, "s")
hs.eventtap.keyStroke({"ctrl", "alt"}, "t")
hs.eventtap.keyStroke({}, "return")
hs.eventtap.keyStroke({}, "escape")
hs.eventtap.keyStroke({}, "f1")
hs.eventtap.keyStroke({"cmd"}, "f5")
hs.eventtap.leftClick({x=100, y=200})
hs.eventtap.rightClick({x=100, y=200})
hs.eventtap.middleClick({x=100, y=200})
hs.eventtap.doubleClick({x=100, y=200})
hs.mouse.absolutePosition({x=500, y=300})
hs.eventtap.scrollWheel({0, -5}, {})
hs.eventtap.scrollWheel({0, 5}, {})
Running from CLI
hs -c 'hs.eventtap.keyStroke({"cmd"}, "c")'
hs /path/to/test_script.lua
echo 'hs.eventtap.keyStrokes("test")' | hs -s
Application Control
hs.application
local app = hs.application.launchOrFocus("Safari")
hs.application.launchOrFocusByBundleID("com.apple.Safari")
local app = hs.application.get("Safari")
if app then
app:activate()
app:hide()
app:unhide()
app:kill()
app:kill9()
end
local front = hs.application.frontmostApplication()
print(front:name())
print(front:bundleID())
for _, app in ipairs(hs.application.runningApplications()) do
print(app:name())
end
hs.timer.waitUntil(
function() return hs.application.get("MyApp") ~= nil end,
function() print("App launched") end,
0.5
)
Menu Items
local app = hs.application.get("Safari")
app:selectMenuItem({"File", "New Window"})
app:selectMenuItem({"Edit", "Paste"})
local menuItem = app:findMenuItem({"File", "Save"})
if menuItem then
print("Save is available, enabled:", menuItem.enabled)
end
Window Management
hs.window
local win = hs.window.focusedWindow()
print(win:title())
print(win:frame())
local app = hs.application.get("Safari")
local wins = app:allWindows()
for _, win in ipairs(wins) do
print(win:title())
end
local win = hs.window.get("My Document")
win:focus()
win:maximize()
win:minimize()
win:close()
win:setFrame({x=100, y=100, w=800, h=600})
win:move({100, 0})
win:setSize({800, 600})
win:centerOnScreen()
local frame = win:frame()
print("Position:", frame.x, frame.y)
print("Size:", frame.w, frame.h)
Screenshots
### The Iron Law of Visual Verification
Every E2E test MUST include screenshot evidence.
After completing a workflow, capture a screenshot to prove success.
screencapture (CLI)
screencapture /tmp/screenshot.png
screencapture -m /tmp/main_screen.png
screencapture -w /tmp/window.png
screencapture -R 100,200,800,600 /tmp/region.png
screencapture -o /tmp/no_shadow.png
screencapture -x /tmp/silent.png
screencapture -c
screencapture -x -o -R 0,0,1920,1080 /tmp/clean.png
hs.screen (Hammerspoon)
local win = hs.window.focusedWindow()
if win then
local img = win:snapshot()
img:saveToFile("/tmp/window.png")
end
local screen = hs.screen.mainScreen()
local img = screen:snapshot()
img:saveToFile("/tmp/screen.png")
local img = hs.screen.mainScreen():snapshot({x=0, y=0, w=800, h=600})
img:saveToFile("/tmp/region.png")
Complete E2E Example
### E2E Test Structure
Every Hammerspoon E2E test MUST:
- Launch - Start the application
- Verify launch - Assert app is running
- Interact - Perform user actions
- Verify state - Check expected state (clipboard, window, etc.)
- Screenshot - Capture visual evidence
- Cleanup - Close app, restore state
local function test_app_workflow()
print("Launching app...")
hs.application.launchOrFocus("TextEdit")
hs.timer.usleep(1000000)
local app = hs.application.get("TextEdit")
assert(app, "FAIL: TextEdit did not launch")
print("App launched: " .. app:name())
hs.eventtap.keyStroke({"cmd"}, "n")
hs.timer.usleep(500000)
hs.eventtap.keyStrokes("Hello, this is an automated test!")
hs.timer.usleep(300000)
hs.eventtap.keyStroke({"cmd"}, "a")
hs.timer.usleep(100000)
hs.eventtap.keyStroke({"cmd"}, "c")
local clipboard = hs.pasteboard.getContents()
assert(clipboard:find("automated test"), "FAIL: Clipboard doesn't match")
print("Clipboard verified: " .. clipboard)
local win = hs.window.focusedWindow()
local img = win:snapshot()
img:saveToFile()
()
hs.eventtap.keyStroke({}, )
hs.timer.usleep()
hs.eventtap.keyStroke({}, )
()
, err = (test_app_workflow)
( .. (err))
.()
.()
Run from CLI:
hs /path/to/test_workflow.lua && echo "TEST PASSED" || echo "TEST FAILED"
Alternative: cliclick
For simpler needs, cliclick provides CLI-based mouse/keyboard control:
cliclick c:100,200
cliclick rc:100,200
cliclick dc:100,200
cliclick m:500,300
cliclick t:"Hello world"
cliclick kp:return
cliclick kp:escape
cliclick kd:cmd kp:c ku:cmd
cliclick w:500
cliclick is useful for simple scripts but lacks app control - prefer Hammerspoon for complex E2E tests.
Output Requirements
Every test run MUST be documented in LEARNINGS.md:
## macOS E2E Test: [Description]
**Tool:** Hammerspoon
**Script:**
```bash
hs /path/to/test_script.lua
Output:
Launching app...
App launched: TextEdit
Clipboard verified: Hello, this is an automated test!
Screenshot saved to /tmp/test_result.png
PASS: Workflow completed successfully
Result: PASS
Screenshot: /tmp/test_result.png
## Integration
This skill is referenced by `dev-test` for macOS desktop automation.
Read `${CLAUDE_SKILL_DIR}/../../skills/dev-tdd/SKILL.md` and follow its instructions.