Skip to content

Troubleshooting

This page covers solutions to common problems. If yours is not listed, feel free to open an issue on GitHub.


Hammerspoon requires Accessibility permissions to manage windows.

  1. Open System Settings (or System Preferences on older macOS)
  2. Navigate to Privacy & Security > Accessibility
  3. Ensure Hammerspoon is listed and enabled
  4. If enabled but not working, remove and re-add it

After changing permissions, reload Hammerspoon config (Cmd+Alt+Ctrl+R) or restart Hammerspoon.

Open the Hammerspoon console (Cmd+Alt+C) and look for errors.

Common errors:

Error What it means
module 'Shoji' not found Shoji is not in the expected location
attempt to call a nil value API usage error or version mismatch
invalid config key Typo in configuration option name

Shoji requires macOS 12 (Monterey) or later and Hammerspoon 1.0.0+. See the compatibility matrix for full details.

Check the installation path:

-- Check Spoon directory
print(hs.spoons.installDirectory)
-- Expected: ~/.hammerspoon/Spoons/
-- Check if Shoji is found
print(hs.spoons.scriptPath("Shoji"))
-- Should show path to Shoji.spoon

Paste this into the Hammerspoon console:

print(spoon.Shoji._started) -- Should print: true

If you see false or nil, Shoji has not started. Add spoon.Shoji:start() to your config and reload.

Apps in filter_apps may be excluded from tiling:

print("Mode: " .. spoon.Shoji.config.filter_mode)
print("Apps: " .. hs.inspect(spoon.Shoji.config.filter_apps))
  • blocklist mode: listed apps are excluded
  • allowlist mode: only listed apps are tiled

Windows below the minimum size are excluded:

print("Min width: " .. spoon.Shoji.config.min_width)
print("Min height: " .. spoon.Shoji.config.min_height)

Set these to 0 to disable size filtering.

Floating windows are excluded:

local win = hs.window.focusedWindow()
if win then
print(spoon.Shoji.state:isFloating(win:id()))
end

Use toggle_float to change the floating state.


Another app or Hammerspoon binding may be using the same hotkey:

for _, hk in ipairs(hs.hotkey.getHotkeys()) do
print(hk.idx, hk.msg)
end
print(hs.inspect(spoon.Shoji._hotkeys))

If empty, hotkeys were not bound:

spoon.Shoji:bindHotkeys(
spoon.Shoji.actions.DEFAULT_HOTKEYS
)

Bypass the hotkey and call the action directly:

spoon.Shoji.actions.focusLeft()
spoon.Shoji.actions.cycleLayoutForward()

If the action works, the problem is with the hotkey binding, not the action.


Force a retile:

spoon.Shoji:retile()

Or reset layout parameters (ratio and main_count) to defaults:

spoon.Shoji.actions.resetSpace()

Check that the layout is in enabled_layouts:

print(hs.inspect(spoon.Shoji.config.enabled_layouts))

Layouts not in this list cannot be activated via cycle_layout_forward. Use the set_layout_<name> actions to switch directly to any registered layout.

Reset BSP state (clears split ratios and rebuilds the tree):

spoon.Shoji.actions.resetSpace()

Profile a retile:

local start = hs.timer.absoluteTime()
spoon.Shoji:retile()
local elapsed = (hs.timer.absoluteTime() - start) / 1000000
print(elapsed .. "ms")

Typical retile takes under 50ms. If slow:

  • Check hooks for expensive operations
  • Reduce window count on the space

Shoji uses several timing constants to balance responsiveness with stability:

Constant Value Purpose
System coalescing 50ms Coalesces bursty system events
Debounce delay 100ms Coalesces rapid window events
Window created delay 100ms Allows window init before tiling
Event suppression 150ms Ignores self-triggered events during tiling
Space switch retile 300ms Delay before retile on space switch
Max debounce wait 500ms Prevents starvation during rapid events
Recently tiled TTL 500ms Per-window tile tracking window
Event suppression max 2s Maximum suppression duration

These values are not user-configurable but are tuned for typical use cases. If you experience issues with specific apps, consider using the blocklist.

Shoji caches window-to-space mappings and window positions to avoid expensive Hammerspoon API calls. Caches are automatically invalidated when:

  • Windows are created, destroyed, or moved
  • Screens are connected or disconnected
  • Spaces are changed

If you suspect stale cache data, force a retile:

spoon.Shoji:retile()

Install the Hammerspoon CLI (installs to /usr/local/bin/):

hs.ipc.cliInstall()

Verify Shoji is loaded and started:

Terminal window
hs -c 'print(spoon.Shoji._started)'

If nil, check the Hammerspoon config (init.lua):

hs.loadSpoon("Shoji")
spoon.Shoji:configure({ ... })
spoon.Shoji:start()

spoon.Shoji.logger = hs.logger.new("Shoji", "debug")

Log levels: error, warning, info, debug, verbose

Open the Hammerspoon console (Cmd+Alt+C). Log messages appear in real time.

local spaceID = hs.spaces.focusedSpace()
print("macOS Space: " .. spaceID)
print("Layout: " .. spoon.Shoji.state:getSpaceLayout(spaceID))
print("Windows: " .. hs.inspect(spoon.Shoji.state:getWindowOrder(spaceID)))
print("Ratio: " .. spoon.Shoji.state:getMainRatio(spaceID))

Reload the Hammerspoon configuration:

hs.reload()

Or click the Hammerspoon menu bar icon and select Reload config.


If you see Hook recursion limit reached:

hooks = {
after_tile = function(spaceID)
spoon.Shoji:retile() -- This creates infinite recursion!
end,
}

Don’t call tiling functions from tiling hooks. The recursion limit (10) exists to prevent infinite loops.

Hooks are wrapped in pcall so errors don’t crash Shoji. Check the console:

spoon.Shoji.logger = hs.logger.new("Shoji", "debug")

Look for [ERROR] Hook <name> failed: messages.


Each macOS Space is tied to a specific screen. Shoji tiles per macOS Space, not per screen. If a window appears on the wrong screen, check which macOS Space it belongs to:

local win = hs.window.focusedWindow()
print(hs.inspect(hs.spaces.windowSpaces(win)))

Each macOS Space maintains independent layout state. Switching between macOS Spaces on different screens does not reset layouts. The previous state is preserved.


Some apps create transient windows that trigger tiling events. Add them to the blocklist:

spoon.Shoji:configure({
filter_mode = "blocklist",
filter_apps = {
"com.apple.systempreferences", -- System Settings dialogs
"com.apple.finder", -- Finder dialogs
},
})

Some Electron apps report incorrect initial sizes. Force a retile if layouts appear incorrect after focusing these windows:

spoon.Shoji:retile()

Windows reposition after I move them manually

Section titled “Windows reposition after I move them manually”

This is expected behavior. Shoji is a tiling window manager, so it reclaims window positions on the next tiling event. Any window event (creation, destruction, focus change, space switch) triggers a retile that repositions all managed windows according to the active layout.

To exempt a window from tiling, toggle it to floating:

-- Toggle the focused window's floating state
spoon.Shoji.actions.toggleFloat()

Or bind toggle_float to a hotkey in your configuration. Floating windows are excluded from tiling and can be positioned freely.


Shoji uses intentional delays to ensure stability:

Delay Value Why it exists
System coalescing 50ms Groups bursty system events into one retile
Debounce delay 100ms Coalesces rapid window events
Window created delay 100ms Lets windows finish initializing before tiling
Max debounce wait 500ms Prevents starvation during rapid events

These delays mean a retile may not happen instantly after a hotkey press, especially if other window events are firing simultaneously. The debounce prevents redundant retiles (e.g., when an app creates multiple windows at once) and the window creation delay prevents tiling a window before it has reported its final size.

These values are not user-configurable. If a specific app causes excessive retiling, add it to the blocklist instead.


Each macOS Space maintains independent layout state (layout name, main ratio, main count, window order). This state is persisted to a JSON file and restored on restart.

The state file location follows XDG conventions:

Terminal window
# Default location
cat "${XDG_STATE_HOME:-$HOME/.local/state}/shoji/state.json" \
| python3 -m json.tool

If you configured a custom path via state_path, check that location instead.

Shoji identifies spaces by a stable key composed of the screen UUID and the space index on that screen (e.g., UUID:2 for the second space on a screen). If you add or remove spaces, the index shifts and Shoji may map persisted state to the wrong space.

Each persisted space entry includes a fingerprint (window count, layout name, screen UUID). On restore, Shoji compares the saved fingerprint against the current state. A mismatch is logged as a warning but does not prevent restoration. Check the Hammerspoon console for fingerprint mismatch messages.

To reset a single space to defaults:

spoon.Shoji.actions.resetSpace()

To reset all persisted state, delete the state file and reload:

os.remove(spoon.Shoji.persistence:getPath())
hs.reload()

Floating state is tracked by window ID, which is an integer assigned by macOS when a window is created. Each window instance gets a unique ID. When you close a window, that ID is gone. Reopening the app creates a new window with a different ID, so it has no floating flag and is tiled normally.

This is by design: floating is a per-window-instance property, not a per-app property. To permanently exclude an app from tiling, add its bundle ID to the blocklist:

spoon.Shoji:configure({
filter_mode = "blocklist",
filter_apps = {
"com.example.myapp",
},
})

Shoji persists layout state (layout name, main ratio, main count, window order, and layout-specific state like BSP split ratios) across restarts. If your layouts are not being restored, check the following.

Check for save/load messages in the Hammerspoon console:

spoon.Shoji.logger = hs.logger.new("Shoji", "debug")
-- Reload and look for:
-- "State saved to ..."
-- "State loaded from ..."
Terminal window
ls -la "${XDG_STATE_HOME:-$HOME/.local/state}/shoji/state.json"

If the file does not exist, the directory may not be writable. Shoji creates the default XDG directory automatically but will not create directories for custom paths configured via state_path.

The state file includes a version number. If you upgrade Shoji and the schema version changes, the old state file is discarded with a warning:

Discarding state file with version 1 (expected 2)

This is intentional — incompatible state data could cause crashes. The layout will reset to defaults after a schema upgrade.

On restore, Shoji compares a saved fingerprint (window count, layout, screen UUID) against current state. If the fingerprint does not match (e.g., you had 3 windows when you quit but only 2 on restart), Shoji logs a warning but still applies the saved layout. Window order is pruned to remove IDs that no longer correspond to open windows.


Paste this snippet into the Hammerspoon console (Cmd+Alt+C) to gather diagnostic information:

local s = spoon.Shoji
local ver = hs.host.operatingSystemVersion()
print("macOS: " .. ver.major .. "." .. ver.minor
.. "." .. ver.patch)
print("Hammerspoon: " .. hs.processInfo.version)
print("Shoji: " .. (s.version or "unknown"))
print("Started: " .. tostring(s._started))
print("Debug logging: " .. tostring(
s.logger and s.logger.getLogLevel
and s.logger:getLogLevel() >= 4
))
local spaceID = hs.spaces.focusedSpace()
print("Space: " .. spaceID)
print("Layout: " .. s.state:getSpaceLayout(spaceID))
local order = s.state:getWindowOrder(spaceID)
print("Managed windows: " .. #order)

The output shows:

  • macOS / Hammerspoon / Shoji — version strings to rule out compatibility issues
  • Started — true if Shoji is running; false or nil means spoon.Shoji:start() was not called
  • Debug logging — whether the log level is debug or higher
  • Space / Layout / Managed windows — current tiling state for the focused macOS Space

Include this output when opening a GitHub issue. To enable debug logging for a more detailed report:

spoon.Shoji.logger = hs.logger.new("Shoji", "debug")

If none of the above resolves the problem:

  1. Search GitHub Issues for similar problems
  2. Open a new issue with:
    • Diagnostics output from the snippet above
    • Minimal config that reproduces the problem
    • Console output and errors