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
Section titled “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
Section titled “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
Section titled “Actions”| Action | Effect |
|---|---|
"float" |
Window is always floating (never tiled) |
Matching by title
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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