Debug: Hammerspoon Workspace Search

By Jay Griffin, Claude Opus 4.6*Claude Opus 4.6 via GitHub CopilotΒ·Β  March 4, 2026
docs
🏷️ Tags:debughammerspoonmacos

Debugging the Hammerspoon workspace search script β€” data model, known issues, and console log audit guide.

How It Works

The workspace search lives in ~/.hammerspoon/init.lua. Two hotkeys:

Data Model

Workspaces are stored in hs.settings under the key "workspaces". Each entry looks like:

.json
{
  "name": "reading",
  "spaces": {
    "SCREEN-UUID-1": 42,
    "SCREEN-UUID-2": 57
  }
}

The spaces table maps each screen's UUID to the macOS space ID that was active on that screen when saved. On switch, the script calls hs.spaces.gotoSpace(spaceID) for each entry.

Known Failure Modes

1. Space IDs Are Ephemeral

macOS assigns integer space IDs at boot. They change on restart, and can change when spaces are added/removed/reordered. A workspace saved yesterday may have stale IDs today. The script detects this by checking against hs.spaces.allSpaces() and marks stale entries in the chooser.

2. Screen UUIDs Can Change

Screen UUIDs come from screen:getUUID(). If a monitor is unplugged and re-plugged, or display arrangement changes, the UUID may differ. The script would still call gotoSpace with the right space ID, but the UUID key in the saved data won't match the current screen UUID β€” meaning the lookup structure is stale even if the space IDs happen to still be valid.

3. gotoSpace Only Switches the Screen That Owns the Space

With "Displays have separate Spaces" enabled, each screen has its own set of space IDs. Calling hs.spaces.gotoSpace(42) only switches the screen that owns space 42. If both screens need to switch, we need two calls β€” one per screen's saved space ID. The original single-spaceID version only saved one screen's space.

4. gotoSpace May Silently Fail

hs.spaces.gotoSpace() can return success but not actually switch. This happens when: the space exists but macOS is mid-animation, the space belongs to a fullscreen app, or System Integrity Protection blocks the private API. The debug logging prints the pcall result so we can see if the call itself errors.

Console Log Guide

Open the Hammerspoon console (CMD+SHIFT+SPACE on the Hammerspoon menu bar icon, or hs.openConsole()). All workspace operations now print [WS]-prefixed logs.

On Save (CMD+SHIFT+S)

.txt
[WS] Screen: LG HDR 4K (UUID=ABC-123) β†’ spaceID=42
[WS] Screen: Built-in Retina (UUID=DEF-456) β†’ spaceID=57
[WS] Loaded 3 workspaces from settings
[WS]   1. jaygriff β†’ ABC-123=38 DEF-456=51
[WS]   2. reading β†’ ABC-123=42 DEF-456=57
[WS]   3. finance β†’ ABC-123=45 DEF-456=60
[WS] Saving 3 workspaces
[WS] Saved workspace 'reading' with spaces:
[WS]   ABC-123 β†’ 42
[WS]   DEF-456 β†’ 57

On Switch (CMD+SHIFT+SPACE β†’ select)

.txt
[WS] Loaded 3 workspaces from settings
[WS]   1. jaygriff β†’ ABC-123=38 DEF-456=51
[WS]   2. reading β†’ ABC-123=42 DEF-456=57
[WS]   3. finance β†’ ABC-123=45 DEF-456=60
[WS] Screen: LG HDR 4K (UUID=ABC-123) β†’ spaceID=38
[WS] Screen: Built-in Retina (UUID=DEF-456) β†’ spaceID=51
[WS] Switching to workspace: reading
[WS]   gotoSpace(42) for screen ABC-123
[WS]   result: ok=true err=nil
[WS]   gotoSpace(57) for screen DEF-456
[WS]   result: ok=true err=nil

What to Check

Quick Hammerspoon Console Commands

-- Dump all current space IDs per screen
for uuid, spaces in pairs(hs.spaces.allSpaces()) do print(uuid, hs.inspect(spaces)) end

-- Check what's saved
hs.inspect(hs.settings.get("workspaces"))

-- Nuke all saved workspaces and start fresh
hs.settings.set("workspaces", {})

-- Manually try switching to a specific space ID
hs.spaces.gotoSpace(42)