focus-follows-close

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.

Notes FileEditWindow
Click a window to focus it, its red dot to close it. Or press W to close, to switch, F to toggle.

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.

You do
What happens
Close an app's last window
script acts

Focus moves to the window you used just before it, in MRU order.

Close one of several windows
macOS

Left alone — macOS focuses the app's next window itself.

Switch apps, or launch from Spotlight
macOS

Left alone. The closed window belongs to a different app than the one now frontmost, so the guard declines.

The next window is on another Space
macOS

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.

01

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.

02

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.

03

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.

203lines of Lua, self-contained
1case it acts on
60windows kept in the MRU
~57mssaved per close by holding objects
0flicker, by construction

Configuration

Five knobs, at the top of the file

Edit and save — the config reloads itself.

SettingDefaultMeaning
M.enabledtrueMaster switch, also bound to the hotkey.
M.sameSpaceOnlytrueNever focus a window on a hidden Space.
M.toggleMods / M.toggleKey⌘⌥⌃ + FThe on/off hotkey. F for "follows".
M.skipFocusInto{}Apps never to land focus on, e.g. Finder.
M.skipTriggerFromHammerspoon, Spotlight, AlfredApps 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.sh

Manual

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.lua

Hands-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