macOS · Hammerspoon · 203 lines of Lua
Close the last window. Land somewhere useful.
macOS is application-centric. Close an app's last window and it keeps that now-empty app frontmost, focusing nothing at all. This moves focus to the window you were using before — and touches nothing else.
On your Mac those are ⌘W and ⌘⌥⌃F. This demo uses bare keys so it cannot close your browser tab.
The gap
One case, and macOS has no setting for it
Windows and most Linux desktops are window-centric: focus belongs to a window. macOS gives it to an application, which is usually fine and occasionally leaves you nowhere.
Close one of several windows of an app and macOS focuses that app's next window. That is correct, and this script never touches it.
Close an app's last window and macOS keeps the windowless app frontmost — the menu bar still reads its name — while focusing nothing. Your next keystroke goes to an app with no window to put it in, and you have to reach for the mouse or Command-Tab to get back to work. There is no native preference for this. That is the entire gap this fills.
What it touches
Almost always, nothing
The restraint is the design. Because it never overrides macOS's own reflexes, there is nothing to fight and nothing to flash.
Focus moves to the window you used just before it, in MRU order.
Left alone — macOS focuses the app's next window itself.
Left alone. The closed window belongs to a different app than the one now frontmost, so the guard declines.
Skipped, so a close never yanks you to another desktop. Toggle with sameSpaceOnly.
Why there is no flicker. Flicker comes from overriding macOS's habit of raising another same-app window on close: it raises one, you raise another, and the screen shows both. This script stays out of the multi-window case entirely and only fills the windowless one — where macOS raises nothing, so there is nothing to flash.
How it works
Three small ideas
Plus one measurement that decided the data structure.
Its own MRU list
macOS z-order is an unreliable record of what you used last, so the script keeps its own order from windowFocused events — up to 60 windows.
A windowless check
It acts only when the app whose window you closed has no other standard window left. The check excludes the just-closed window's id, to dodge a teardown timing race.
An app-match guard
It acts only when the closed window's app is still frontmost. App switches and Spotlight launches fail that test, so the script stays silent.
Why window objects, not ids. The MRU stores window objects rather than identifiers. Validating a stored object is sub-millisecond; resolving an id back to a window through hs.window.get re-enumerates every window on the system and costs about 57 ms — which you would feel on every close.
Configuration
Five knobs, at the top of the file
Edit and save — the config reloads itself.
| Setting | Default | Meaning |
|---|---|---|
| M.enabled | true | Master switch, also bound to the hotkey. |
| M.sameSpaceOnly | true | Never focus a window on a hidden Space. |
| M.toggleMods / M.toggleKey | ⌘⌥⌃ + F | The on/off hotkey. F for "follows". |
| M.skipFocusInto | {} | Apps never to land focus on, e.g. Finder. |
| M.skipTriggerFrom | Hammerspoon, Spotlight, Alfred | Apps whose closes are ignored entirely. |
Install
Needs Hammerspoon and one permission
macOS with Hammerspoon 0.9.90 or newer — it uses hs.spaces — and Accessibility granted to Hammerspoon. Without that permission the script is inert rather than broken.
Recommended
The installer
Installs Hammerspoon via Homebrew if missing, backs up any existing init.lua, places the script, launches Hammerspoon, then reloads and verifies.
git clone https://github.com/romankhadka/focus-follows-close.git
cd focus-follows-close
./install.shManual
Drop the file in
Use it as your whole config, or require it from the one you have.
brew install --cask hammerspoon
mkdir -p ~/.hammerspoon
cp focus-follows-close.lua ~/.hammerspoon/init.lua
# or, alongside an existing config:
cp focus-follows-close.lua ~/.hammerspoon/
echo 'require("focus-follows-close")' >> ~/.hammerspoon/init.luaHands-off
With Claude Code
The repo ships a skill. Copy it into your global skills directory and ask Claude to set up focus-follows-close — it does the install and the verification.
mkdir -p ~/.claude/skills/focus-follows-close
cp skills/focus-follows-close/SKILL.md \
~/.claude/skills/focus-follows-close/Check
Verify it took
Open Notes and Safari with one window each, Notes frontmost. Close the Notes window — focus should land on Safari. From a shell:
hs -c "tostring(_G.FocusFollowsClose.enabled)" # true
hs -c "tostring(#_G.FocusFollowsClose.mru)" # tracked windows
hs -c "tostring(hs.accessibilityState())" # true