349 points by tosh 2 days ago | 141 comments on HN
| Moderate positive Moderate agreement (3 models)
Mixed · v3.7· 2026-03-15 22:54:20 0
Summary Free Expression & Association Advocates
Hammerspoon is an open-source macOS automation tool hosted on GitHub. The repository's structure strongly supports Articles 19 (free expression) and 20 (free association) through public accessibility, collaborative contribution mechanisms, and absence of censorship or gatekeeping. The evaluation reflects moderate positive signals on freedom of movement, equality of access, and education—primarily through design rather than explicit editorial stance. No antagonistic content or structural barriers to human rights were observed.
Rights Tensions1 pair
Art 12 ↔ Art 19 —GitHub's default-allow tracking model (no cookie consent required per DCP) potentially enables surveillance that could chill free expression, though actual tracking is minimal and absent of third-party collectors.
i've tried all of the other fancy window managers and for me nothing has ever beat the ease of use of just
(1) ctrl-d to see the grid, (2) type the letter where you want the top left corner of your window to be, (3) type the letter where you want the bottom right corner to be
It's fun to combine with qmk [0], which gives you a bunch more options for hotkeys on your keyboard via layers. I've ended up with a layer where half the keyboard is Hammerspoon shortcuts directly to apps (e.g. go to Slack, to Chrome, etc.) and half of it is in-app shortcuts (like putting cmd-number on the home row, for directly addressing chrome tabs).
Between this and one of the tiling window manager-adjacent tools (I use Sizeup), I can do all my OS-level navigation directly. "Oh I want to go to Slack and go to this DM" is a few keystrokes away, and not dependent on what else I was doing.
Hammerspoon is the glue that holds my Mac together. For a starter list of things to do with this app, a partial list of the things that I'm using it for:
- Dumping all open Safari tabs to an Obsidian doc
- Adding 'hyper' (Ctrl-Opt-Cmd) keybinds to pop a new window for:
- Safari
- Finder
- Terminal / Ghostty
- VS Code
- Notes
- Editing Hammerspoon/AeroSpace/Sketchybar config
- Reloading Hammerspoon config
- Reloading Sketchybar
- Quitting all Dock apps except Finder
- Screen lock
- System sleep
- Opening front Finder folder in VS Code
- Opening front Safari URL on Archive.today
- Showing front Safari window tab count
- Showing front app bundle ID
- Posting notification about current Music track
- Controlling my Logi Litra light (various color temps/brightnesses)
- Starting/stopping a client work timer
- Tying it to AeroSpace for:
- Pushing a window to another monitor
- Performing a two-up window layout
- Swapping those two windows
- Closing all other workspace windows
- Gathering all windows to first workspace
- Ensuring some background apps stay running if they crash
- Prompting to unmount disk images if trashed
- Binding into Skim to jump to specific sections of spec PDFs using terse Markdown URLs
I fake a tiling window manager on Mac with Hammerspoon, resizing to fit in specific corners/sizes:
-- resize based on ratios
function ratioResize(xr, yr, wr, hr)
return function ()
local win = hs.window.focusedWindow()
win:moveToUnit({x=xr,y=yr,w=wr,h=hr})
end
end
-- 4 corners, different sizes
hs.hotkey.bind({"cmd", "ctrl"}, "w", ratioResize(0, 0, 2/5, 2/3))
hs.hotkey.bind({"cmd", "ctrl"}, "e", ratioResize(2/5, 0, 3/5, 2/3))
hs.hotkey.bind({"cmd", "ctrl"}, "s", ratioResize(0, 2/3, 2/5, 1/3))
hs.hotkey.bind({"cmd", "ctrl"}, "d", ratioResize(2/5, 2/3, 3/5, 1/3))
And to throw windows to other monitors:
-- send to next screen
hs.hotkey.bind({"cmd", "ctrl"}, ";", function()
local win = hs.window.focusedWindow()
local screen = win:screen()
local next_screen = screen:next()
win:moveToScreen(next_screen)
end)
I use it to hide Zoom's screen sharing controls so they don't come back when pressing Esc:
-- Hide Zoom's "share" windows so it doesn't come back on ESC keypress
local zoomWindow = nil
local originalFrame = nil
hs.hotkey.bind({"cmd", "ctrl", "alt"}, "H", function()
print("> trying to hide zoom")
if not zoomWindow then
print("> looking for window")
zoomWindow = hs.window.find("zoom share statusbar window")
end
if zoomWindow then
print("> found window")
if originalFrame then
print("> restoring")
zoomWindow:setFrame(originalFrame)
originalFrame = nil
zoomWindow = nil
else
print("> hiding")
originalFrame = zoomWindow:frame()
local screen = zoomWindow:screen()
local frame = zoomWindow:frame()
frame.x = screen:frame().w + 99000
frame.y = screen:frame().h + 99000
zoomWindow:setFrame(frame)
end
else
print("> window not found")
end
end)
local appHotkeys = {}
local function remapAppHotkey(appName, fromMods, fromKey, toMods, toKey, delay)
if not appHotkeys[appName] then
appHotkeys[appName] = {}
end
local hotkey = hs.hotkey.new(fromMods, fromKey, function()
hs.eventtap.keyStroke(toMods, toKey, delay or 0)
end)
table.insert(appHotkeys[appName], hotkey)
end
local appWatcher = hs.application.watcher.new(function(appName, eventType)
local hotkeys = appHotkeys[appName]
if not hotkeys then return end
for _, hotkey in ipairs(hotkeys) do
if eventType == hs.application.watcher.activated then
hotkey:enable()
elseif eventType == hs.application.watcher.deactivated then
hotkey:disable()
end
end
end)
appWatcher:start()
-- Remap app hotkeys
remapAppHotkey("Finder", { "cmd" }, "q", { "cmd" }, "w", 0.5)
... etc ...
I tried to find a proper window control tool for macOS for a while, tested Rectangle and Magnet and dunno how many.
Then I just figured out that I have Hammerspoon, it can control windows -> recreate one exactly how I like it. Been using it for a year now and it's 99% perfect. Some specific applications (coughFirefoxcough) sometimes get into a weird state that doesn't work, but I can live with that.
It can also pop all windows to a specific layout with a single shortcut by combining the active wifi + monitor setup to detect if I'm at home, at work, or working at home.
I can't even work on Mac without it. It let's you do stuff like "alt+spc a b" (apps -> browser) or "alt+spc m j/k" (media -> vol up/down), or edit just about any text of any app in your editor (Emacs atm) - with all the tools you have there - spellchecking, thesaurus, translation, LLMs, etc.
You can plug it to your favorite WM (I'm currently using Yabai) and do tons of other interesting things. Because it's all written in Fennel, one can develop things in a tight feedback loop with a connected REPL - e.g., I can ask Claude to inspect things in the running Slack app or Firefox and make interesting automations - all without ever leaving my editor.
Hammerspoon maintainer here - I'm enjoying reading all the comments, and hoping that everyone isn't going to be annoyed that I'm mostly working on a v2 atm, which switches from Lua to JavaScript :D
I recently set up Hammerspoon to surveil my own computer usage actions (active tab/window, typing state, scrolling) to have a next-action predictor. It shows the predicted next action at the top of the screen but I was thinking of using it to improve voice command accuracy.
Coming from windows to macos, I(i think i used perplexity :P) created a spoon for switching between open windows with 4 finger swipe[0]. Swiping left/right switches between windows, swiping down minimizes all visible windows, swiping up restores them(one by one). Created this repo to backup my config with an llm documenting it.
It uses a swipe gesture detection spoon I found after searching for something similar[1].
Glad to see other people using it. Saved my life, was going crazy click-clicking to nab the right window. Now Cmd-1..9 brings to focus a window of my chosen application. (Chrome) In case it helps someone else, myself and Codex iterating over time https://github.com/ljubomirj/dotfiles/blob/main/.hammerspoon....
Cmd-1..9 switches over focuses to a particular window, Cmd-0 presents an (ugly; but suffices) dialog box to select the window with arrows (of the App of interest - Chrome for me atm) to switch to. But more important - to see what window what Window name is recalled by the particular Cmd-1..9 shortcut. Option-arrows shuffle window-to-key ordering. I right-click-Name Window my windows. Think back now - on restart they may even be preserved?? Don't recall re/naming them manually recently. (possible I've forgotten though)
I used to be a fan of tiling window managers, but I found out that I tend to use fairly visually heavy apps on a Mac. By this I mean apps that need quite a bit of screen real estate to show everything that needs to be shown:
The mail program has a folder tree on the left, the list of messages in the center, and the current message on the right. The IDE has all these tool windows that need showing, in addition to the actual editor. Websites also like it if the window size is a bit more.
Back when I was using Emacs and xterm, mainly, it was nice to show Emacs in the left half and then two xterms on the right.
So instead of tiling, I've come to realize that I only need a couple of window positions and sizes: Mail program and IDE are full screen. The browser occupies 70% width and height, in the top right corner, and the terminal is in the bottom left corner, 200 columns by 44 rows or so. (Lazygit works better if the terminal is a bit larger.) The chat program is full height, 60% width, left edge.
In this way, while the IDE is building or running tests, I can summon the web browser and still see at the bottom and on the left what is the progress of build or test. Also, when I use the software through the browser, I can see a couple of lines of log messages, which is enough to tell me whether to switch.
So I'm now happy with hotkeys in Hammerspoon that reposition and resize the current window to one of these presets, and to jump to a specific app with a keypress. I use a modal for this.
I dig the idea of having multi-level modals, somehow this idea never occurred to me.
This is more powerful than Mac’s own Automator tool, Lua is an interesting choice. Mac Automator used to only support AppleScript but now it supports JavaScriptCore from WebKit so you can run JavaScript with Mac Automator. This is what I do.
I’ll have a hell of a time rewriting everything into Lua when I have soooo many node packages I leverage.
This might be a dumb quesiton but ... do you think AppleScript was a mistake on Apple's part? My understanding is MacOS has had automation via AppleScript forever, but everytime I've used it the syntax is inscrutable. It feels like if they had chosen Python or JavaScript or Lua we'd see much more automation over the last 20+ years. Is Hammerspoon filling that gap?
This is amazing! I have a slightly more elaborate setup that allows me to resize from one or another side, similar to what Apple added recently but with more flexibility, but this is super interesting, thanks for sharing!
Neat until you need to sync configs or keep multiple machines in harmony, at which point dotfile headaches stack up with Hammerspoon and Lua. Adding complex logic like window rules, app-specific behavior, or handling monitor changes strips away some of that hotkey simplicity and leads to endless tweaking. Still, for avoiding the mouse, it's one of the few flexible options left on macOS that doesn't feel ancient. Tradeoffs everywhere but nowhere else really compares in control.
I highly recommend Aerospace[1], went through a few approaches, I cared about not completely compromising security either, it works really well if you come from something like i3
I use it similarly, but I add spots for side x side as well as left, center, right. I only use Hammerspoon for this and a couple tiny things, but it's completely worth it for this alone. Use math to specify window sizes & location. Insanity.
local mode = hs.screen.primaryScreen():currentMode()
local mods = {"ctrl", "alt", "cmd"} -- mash those keys
-- regular app windows
do
local w = 1094 -- no clip on GitHub, HN
local h = 1122 -- tallish
local x_1 = 0 -- left edge
local x_2 = math.max(0, (mode.w - w - w) / 2) -- left middle
local x_3 = (mode.w - w) / 2 -- middle
local x_4 = math.min(mode.w - w, x_2 + w + 1) -- right middle
local x_5 = mode.w - w -- right edge
local y = 23 -- top of screen below menu bar
hs.hotkey.bind(mods, "2", function() move_win( 0, y, mode.w, mode.h) end) -- max
hs.hotkey.bind(mods, "3", function() move_win(x_1, y, w, h) end)
hs.hotkey.bind(mods, "4", function() move_win(x_2, y, w, h) end)
hs.hotkey.bind(mods, "5", function() move_win(x_3, y, w, h) end)
hs.hotkey.bind(mods, "6", function() move_win(x_4, y, w, h) end)
hs.hotkey.bind(mods, "7", function() move_win(x_5, y, w, h) end)
end
function move_win(x, y, w, h)
hs.window.focusedWindow():setFrame(hs.geometry.rect(x, y, w, h))
end
if only keyboards came with built in buttons for adjusting the volume… oh wait. Unless of course you are suffering on a touch bar mac, then I completely understand.
> - Dumping all open Safari tabs to an Obsidian doc
I'd love to do this too. Would you mind sharing how you do it? Or is it trivially easy and not worth explaining? (I haven't looked too deeply into HS yet.)
Wow... that's... incredible. I've used Hammerspoon forever and never knew that existed.
Just messing around I found you can extend the grid size with `hs.grid.setGrid('4x4')`, which you also may then want to shrink the text size with `hs.grid.ui.textSize = 30`, and finally if you use an alternative keyboard layout (eg: Colemak), you can set the grid to use it with `hs.grid.HINTS`. They really thought of everything with this feature.
I pretty much only use it for two (related) things these days:
- check the list of open Teams windows; if there's a non-standard one, assume I'm in a meeting and webhook to HomeAssistant to select the "active"[2] preset on my meeting light[0].
- download my work ical[1] and, if there's a pending meeting (<~15m), webhook-HASS for the "pending" present on the meeting light.
[0] Just a short strip of WS2812B connected to an ESP32 running WLED.
[1] Originally this was a simple HTTP to my shared link on outlook.com but then they started requiring authentication (because that's exactly what you want on a SHARED link, you gufftarts); had a look at the Azure SDK and ... bag of milky spanners that is; ended up having to import my work ical into Apple Calendar and then use the ical link for that in Hammerspoon. Oh how we laughed. Especially when I realised it only has about 40% of the actual meetings because somehow "my calendar" is actually 4 or 5 bastardised conglomerations of pain and the ical for "my calendar" is actually just for one of those. AND NOT THE USEFUL ONE EITHER.
[2] There's various - "camera" for "the one meeting I'm forced to have my camera on", "active" is "I probably have to talk", "passive" is "I'm not going to be talking", and "silent" for things like company presentations where it's just watching a boring Powerpoint over Teams.
Hammerspoon is basically my only reason to write Lua, a language which I really like. I am sure JavaScript is a more pragmatic choice but I will be slightly saddened by it regardless.
Thank you for your work on Hammerspoon! I’ve been using it for years.
Would you mind elaborating on your vision for v2? Was there a certain limitation in the previous architecture that you’re trying to avoid this time around? Was there something in particular that drew you to choosing JavaScript for this version?
I do this too, really happy with my setup - I use hyper+arrow keys to move windows around a monitor (split in thirds on 40”+ or halves on the built-in screen), or jump to another monitor, and hyper+enter to fullscreen. When you push against an edge in full screen it reduces the window size in stages, it all feels natural.
Well, I have been a long user of Hammerspoon, and Lua, so thanks for the great app, it made a difference for me for a long time .. would be happy to hear why, but don’t feel obliged, the switch to JS over Lua, but anyway, thanks again!
This is amazing. Thanks for sharing. Did you ever look into capturing the states of where all the windows are once you are done with resizing them? So as to restore them later back into position if they ever get out of alignment ?
Thinking of the usecase where every task or a project deserves a certain arrangement of windows and it would be good to summon them into existence as and when needed?
Repository existence and GitHub hosting demonstrate voluntary association and assembly. Open-source model inherently enables collective collaboration without compulsion.
FW Ratio: 50%
Observable Facts
Public repository enables unrestricted participation in discussion and contribution workflows.
No visible mandatory membership, identity disclosure requirements, or coercive participation mechanisms.
Forking and independent derivative projects possible under standard GitHub/open-source norms.
Inferences
Repository structure enables voluntary association through collaborative tools.
Open-source licensing permits free association and dissociation without organizational coercion.
Public nature allows group formation around shared technical goals.
Repository name 'Hammerspoon' and description 'Staggeringly powerful macOS desktop automation with Lua' convey technical capability without editorial stance on free expression. Public repository structure enables contributors to share code and ideas freely.
FW Ratio: 50%
Observable Facts
Repository displays public visibility with no apparent access restrictions on viewing or reading content.
Page structure includes discussion, issue, and pull request mechanisms (inferred from GitHub standard layout).
No content filtering, moderation warnings, or censorship indicators visible.
Inferences
Public repository structure with open discussion features supports freedom of expression through technological design.
Absence of tracking reduces surveillance chilling effect on expression.
Open-source software development model inherently practices free exchange of ideas.
GitHub repository structure enables free association through open collaboration: contributors can join development, discussions, issues, and pull requests without gatekeeping. No membership coercion or mandatory participation visible. Contributors can associate or disassociate freely.
GitHub's public repository feature enables unrestricted publication of code, documentation, and discussion. No content moderation, filtering, or censorship visible. DCP +0.05 modifier for no third-party trackers supports privacy dimension of free expression.
GitHub's infrastructure (HTTPS, security headers, no third-party trackers per DCP) supports privacy and secure communication. Platform enables collaborative, transparent software development as public repository.
Public GitHub repository accessible to all users without discriminatory barriers; platform default settings do not restrict access based on protected characteristics.
Public repository structure does not display visible mechanisms that would discriminate by race, color, sex, language, religion, political opinion, national/social origin, property, or birth status.
GitHub's HTTPS encryption, security headers (HSTS, CSP), and infrastructure protect user data integrity and system security. DCP modifier +0.05 for security headers applied.
No cookie consent banner detected per DCP; HTTPS encryption and absence of third-party trackers (per DCP +0.05 modifier) protect privacy. DCP notes no consent mechanism for tracking.
Repository demonstrates intellectual property management through open-source licensing (implied by GitHub public repository); licensing structure protects creator rights while enabling communal access.
Repository functions as educational resource for software development; public code, documentation, and collaborative learning environment enable technical education access. DCP notes 100% alt text indicating accessibility accommodation.
Repository represents technical/cultural commons participation; contributors participate in shared technical culture. DCP notes 100% alt text supports accessibility to cultural content.
GitHub's global infrastructure and open-source community structure enable international cooperation on shared technical problems; repository participates in universal knowledge-sharing system.