Hooks
Hooks run custom code when specific events occur. Use them for logging, integrations, or extending Shoji’s behavior. This page focuses on patterns and examples. For the canonical list of hook names and arguments, see Hooks reference.
At a glance
- Hook names and arguments live in the Hooks reference
- Use
hooks = { event = fn }and arrays for multiple callbacks - Callbacks run in
pcalland are guarded against recursion - Examples below show common patterns
Registering hooks
Via configuration
Pass hooks through configuration:
spoon.Shoji:configure({ hooks = { layout_changed = function(spaceID, layout, prevLayout) print("Layout changed to: " .. layout) end, },})At runtime
Register hooks dynamically with spoon.Shoji:on():
spoon.Shoji:on("after_tile", function(spaceID) print("Tiled space: " .. spaceID)end)This is useful for adding hooks after Shoji has started, or from other Spoons.
Multiple callbacks
Multiple callbacks for the same event:
spoon.Shoji:configure({ hooks = { after_tile = { function(spaceID) print("Tiled space " .. spaceID) end, function(spaceID) hs.alert.show("Retiled!") end, }, },})Hook list
For a complete list of hook names and arguments, see Hooks reference.
Hook aliases
Both snake_case and camelCase work:
hooks = { window_created = fn }hooks = { onWindowCreated = fn } -- equivalentError handling
Callbacks are wrapped in pcall, so errors won’t crash Shoji. Errors log to
the Hammerspoon console.
Recursion guard
Hooks have a depth limit to prevent infinite loops. If a hook triggers an action that fires the same hook, the chain stops after 10 levels.
Examples
Layout-specific wallpaper
local wallpapers = { tall = "~/Pictures/tall.jpg", wide = "~/Pictures/wide.jpg", fullscreen = "~/Pictures/fullscreen.jpg",}
spoon.Shoji:configure({ hooks = { layout_changed = function(_spaceID, layout, _prevLayout) local wallpaper = wallpapers[layout] if wallpaper then hs.screen.mainScreen():desktopImageURL("file://" .. wallpaper) end end, },})Log window events
spoon.Shoji:configure({ hooks = { window_created = function(windowID) local win = hs.window.get(windowID) if win then print("[+] " .. (win:title() or "untitled")) end end, window_destroyed = function(windowID) print("[-] Window " .. windowID .. " closed") end, },})Auto-float specific apps
local autoFloatApps = { ["com.apple.finder"] = true, ["com.apple.Preview"] = true,}
spoon.Shoji:configure({ hooks = { window_created = function(windowID) local win = hs.window.get(windowID) if not win then return end
local app = win:application() if app and autoFloatApps[app:bundleID()] then spoon.Shoji.state:toggleWindowFloating(windowID) end end, },})Alert on retile
spoon.Shoji:configure({ hooks = { after_tile = function(spaceID) hs.alert.show("macOS Space " .. spaceID .. " retiled", 0.3) end, },})