Skip to content

Window rules

Define rules to control individual window behavior without writing Lua hooks. Rules match windows by app bundle ID and/or window title, and apply actions like floating.

Basic usage

spoon.Shoji:configure({
rules = {
{ app = "com.apple.finder", action = "float" },
{ app = "com.apple.systempreferences",
action = "float" },
},
})

Windows matching a rule are treated as floating — they do not participate in tiling and can be freely positioned.

Rule fields

FieldTypeRequiredDescription
appstringno*Bundle ID to match
titlestringno*Substring to match in title
actionstringyesAction to apply

*At least one of app or title must be present.

Actions

ActionEffect
"float"Window is always floating (never tiled)

Matching by title

Match windows by a substring of their title. Title matching is case-insensitive and uses plain text (not patterns):

spoon.Shoji:configure({
rules = {
-- Float Firefox DevTools windows
{ app = "org.mozilla.firefox",
title = "DevTools", action = "float" },
-- Float Slack huddle windows
{ app = "com.tinyspeck.slackmacgap",
title = "Huddle", action = "float" },
},
})

When both app and title are specified, both must match. The title "DevTools" matches any window whose title contains “devtools” (case-insensitive), such as “DevTools - localhost:3000”.

Title-only rules

Rules without an app field match any application:

spoon.Shoji:configure({
rules = {
-- Float any window with "Settings" in the title
{ title = "Settings", action = "float" },
},
})

Rule evaluation

Rules are evaluated in order. The first matching rule wins:

spoon.Shoji:configure({
rules = {
-- This rule is checked first
{ app = "org.mozilla.firefox",
title = "DevTools", action = "float" },
-- This rule is checked second
{ app = "org.mozilla.firefox", action = "float" },
},
})

In this example, all Firefox windows float because the second rule matches any Firefox window. Place more specific rules before general ones.

Rules vs. filters

Rules and filters serve different purposes:

FiltersRules
LevelHammerspoon window filterShoji floating logic
GranularityPer-appPer-app and/or per-title
EffectApp excluded from Shoji entirelyWindow floats but Shoji still tracks it
Configfilter_mode + filter_appsrules

A blocklisted app never reaches rule evaluation. An allowlisted app can still float via a rule — rules are more specific than filters.

spoon.Shoji:configure({
filter_mode = "allowlist",
filter_apps = { "org.mozilla.firefox" },
rules = {
-- Firefox is allowlisted (tiled), but DevTools float
{ app = "org.mozilla.firefox",
title = "DevTools", action = "float" },
},
})

Runtime reconfiguration

Rules take effect immediately when you call configure(). Changing or removing rules updates floating behavior on the next window check:

-- Add rules
spoon.Shoji:configure({
rules = {
{ app = "com.apple.finder", action = "float" },
},
})
-- Later: remove all rules
spoon.Shoji:configure({})

Validation

Rules are validated alongside other configuration. Invalid rules produce clear error messages:

spoon.Shoji:configure({
rules = {
{ action = "float" },
},
})
-- rule[1]: must have at least one of 'app' or 'title'

Only app, title, and action keys are accepted. Unknown keys are rejected to catch typos:

spoon.Shoji:configure({
rules = {
{ app = "com.apple.finder", action = "float",
titel = "typo" },
},
})
-- rule[1]: unknown key 'titel'

Empty strings are also rejected:

spoon.Shoji:configure({
rules = {
{ app = "", action = "float" },
},
})
-- rule[1]: app must not be empty