Troubleshooting
This page covers solutions to common problems. If yours is not listed, feel free to open an issue on GitHub.
Shoji does not start
Section titled “Shoji does not start”Check Hammerspoon permissions
Section titled “Check Hammerspoon permissions”Hammerspoon requires Accessibility permissions to manage windows.
- Open System Settings (or System Preferences on older macOS)
- Navigate to Privacy & Security > Accessibility
- Ensure Hammerspoon is listed and enabled
- If enabled but not working, remove and re-add it
After changing permissions, reload Hammerspoon config (Cmd+Alt+Ctrl+R) or restart Hammerspoon.
Check for Lua errors
Section titled “Check for Lua errors”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 |
Check compatibility
Section titled “Check compatibility”Shoji requires macOS 12 (Monterey) or later and Hammerspoon 1.0.0+. See the compatibility matrix for full details.
Verify Spoon installation
Section titled “Verify Spoon installation”Check the installation path:
-- Check Spoon directoryprint(hs.spoons.installDirectory)-- Expected: ~/.hammerspoon/Spoons/
-- Check if Shoji is foundprint(hs.spoons.scriptPath("Shoji"))-- Should show path to Shoji.spoonWindows are not tiling
Section titled “Windows are not tiling”Verify Shoji is running
Section titled “Verify Shoji is running”Paste this into the Hammerspoon console:
print(spoon.Shoji._started) -- Should print: trueIf you see false or nil, Shoji has not started. Add spoon.Shoji:start()
to your config and reload.
Check window filter settings
Section titled “Check window filter settings”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))blocklistmode: listed apps are excludedallowlistmode: only listed apps are tiled
Check minimum size settings
Section titled “Check minimum size settings”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.
Check if window is floating
Section titled “Check if window is floating”Floating windows are excluded:
local win = hs.window.focusedWindow()if win then print(spoon.Shoji.state:isFloating(win:id()))endUse toggle_float to change the floating state.
Hotkeys do not work
Section titled “Hotkeys do not work”Check for conflicts
Section titled “Check for conflicts”Another app or Hammerspoon binding may be using the same hotkey:
for _, hk in ipairs(hs.hotkey.getHotkeys()) do print(hk.idx, hk.msg)endVerify binding succeeded
Section titled “Verify binding succeeded”print(hs.inspect(spoon.Shoji._hotkeys))If empty, hotkeys were not bound:
spoon.Shoji:bindHotkeys( spoon.Shoji.actions.DEFAULT_HOTKEYS )Test action directly
Section titled “Test action directly”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.
Layout issues
Section titled “Layout issues”Windows overlap or have gaps
Section titled “Windows overlap or have gaps”Force a retile:
spoon.Shoji:retile()Or reset layout parameters (ratio and main_count) to defaults:
spoon.Shoji.actions.resetSpace()Layout does not change
Section titled “Layout does not change”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.
BSP layout behaves unexpectedly
Section titled “BSP layout behaves unexpectedly”Reset BSP state (clears split ratios and rebuilds the tree):
spoon.Shoji.actions.resetSpace()Performance issues
Section titled “Performance issues”Slow retiling
Section titled “Slow retiling”Profile a retile:
local start = hs.timer.absoluteTime()spoon.Shoji:retile()local elapsed = (hs.timer.absoluteTime() - start) / 1000000print(elapsed .. "ms")Typical retile takes under 50ms. If slow:
- Check hooks for expensive operations
- Reduce window count on the space
Internal timing constants
Section titled “Internal timing constants”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.
Caching strategy
Section titled “Caching strategy”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()IPC issues
Section titled “IPC issues”hs command not found
Section titled “hs command not found”Install the Hammerspoon CLI (installs to /usr/local/bin/):
hs.ipc.cliInstall()IPC commands return nil
Section titled “IPC commands return nil”Verify Shoji is loaded and started:
hs -c 'print(spoon.Shoji._started)'If nil, check the Hammerspoon config (init.lua):
hs.loadSpoon("Shoji")spoon.Shoji:configure({ ... })spoon.Shoji:start()Debugging
Section titled “Debugging”Enable debug logging
Section titled “Enable debug logging”spoon.Shoji.logger = hs.logger.new("Shoji", "debug")Log levels: error, warning, info, debug, verbose
View log output
Section titled “View log output”Open the Hammerspoon console (Cmd+Alt+C). Log messages appear in real time.
Inspect current state
Section titled “Inspect current state”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))Force reload
Section titled “Force reload”Reload the Hammerspoon configuration:
hs.reload()Or click the Hammerspoon menu bar icon and select Reload config.
Hook issues
Section titled “Hook issues”Hook recursion warning
Section titled “Hook recursion warning”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.
Hook errors silently failing
Section titled “Hook errors silently failing”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.
Multi-monitor issues
Section titled “Multi-monitor issues”Windows tile on wrong screen
Section titled “Windows tile on wrong screen”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)))Layout resets when switching screens
Section titled “Layout resets when switching screens”Each macOS Space maintains independent layout state. Switching between macOS Spaces on different screens does not reset layouts. The previous state is preserved.
App-specific issues
Section titled “App-specific issues”Dialogs and popups cause retiling
Section titled “Dialogs and popups cause retiling”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 },})Electron apps have incorrect sizes
Section titled “Electron apps have incorrect sizes”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 statespoon.Shoji.actions.toggleFloat()Or bind toggle_float to a hotkey in your configuration. Floating
windows are excluded from tiling and can be positioned freely.
Hotkeys feel slow or have a delay
Section titled “Hotkeys feel slow or have a delay”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.
My layouts are inconsistent across spaces
Section titled “My layouts are inconsistent across spaces”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.
Check persisted state
Section titled “Check persisted state”The state file location follows XDG conventions:
# Default locationcat "${XDG_STATE_HOME:-$HOME/.local/state}/shoji/state.json" \ | python3 -m json.toolIf you configured a custom path via state_path, check that
location instead.
How space identity works
Section titled “How space identity works”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.
Fingerprint mismatches
Section titled “Fingerprint mismatches”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.
Reset a space
Section titled “Reset a space”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 windows I closed came back tiled
Section titled “Floating windows I closed came back tiled”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", },})Layout resets after Hammerspoon restart
Section titled “Layout resets after Hammerspoon restart”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.
Verify persistence is working
Section titled “Verify persistence is working”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 ..."Check the state file exists
Section titled “Check the state file exists”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.
Schema version mismatches
Section titled “Schema version mismatches”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.
Fingerprint validation
Section titled “Fingerprint validation”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.
Getting help
Section titled “Getting help”Collect diagnostics
Section titled “Collect diagnostics”Paste this snippet into the Hammerspoon console (Cmd+Alt+C) to gather diagnostic information:
local s = spoon.Shojilocal 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 —
trueif Shoji is running;falseornilmeansspoon.Shoji:start()was not called - Debug logging — whether the log level is
debugor 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")Report an issue
Section titled “Report an issue”If none of the above resolves the problem:
- Search GitHub Issues for similar problems
- Open a new issue with:
- Diagnostics output from the snippet above
- Minimal config that reproduces the problem
- Console output and errors