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
| Field | Type | Required | Description |
|---|---|---|---|
app | string | no* | Bundle ID to match |
title | string | no* | Substring to match in title |
action | string | yes | Action to apply |
*At least one of app or title must be present.
Actions
| Action | Effect |
|---|---|
"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:
| Filters | Rules | |
|---|---|---|
| Level | Hammerspoon window filter | Shoji floating logic |
| Granularity | Per-app | Per-app and/or per-title |
| Effect | App excluded from Shoji entirely | Window floats but Shoji still tracks it |
| Config | filter_mode + filter_apps | rules |
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 rulesspoon.Shoji:configure({ rules = { { app = "com.apple.finder", action = "float" }, },})
-- Later: remove all rulesspoon.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