Claude Code との相性抜群!Hammerspoon のすすめ
最近 AWS をやめて無職になった jomatsu です。
macOS の操作を自動化・拡張するツールは色々ありますが、個人的なイチオシは Hammerspoon です。Lua スクリプトで OS のあらゆる機能を操作できる自動化フレームワークで、便利さの割に周りで使っている人が少ない印象です。
以前は Lua の敷居の高さがネックでしたが、Claude Code が書いてくれる今は関係ありません。 生成 AI 全盛の今こそ評価されるべきだと考えています。設定は全てコードなので dotfiles で管理できるのも嬉しいポイントです。
百聞は一見にしかず、実例を見てください。
実例
コピー時にトースト通知
コピーしたときに内容をトースト表示します。「ちゃんとコピーできたか」が即座にわかるので地味に安心感があります。これを入れるまで、コピーできていないのが嫌で手癖で「Cmd +C」を 2 連打していました。

クリップボードの変更は hs.pasteboard.changeCount() のポーリングで検知できます。
コード
local lastCount = hs.pasteboard.changeCount()
hs.timer.doEvery(0.15, function()
local cc = hs.pasteboard.changeCount()
if cc == lastCount then return end
lastCount = cc
local text = hs.pasteboard.getContents()
if text and #text > 0 then
hs.alert.show(text:sub(1, 80))
else
hs.alert.show("Copied")
end
end)
hs.alert.show() はシンプルで手軽ですが、見た目がいかにも素っぽいのが気になってきます。Hammerspoon には hs.canvas という描画 API があり、これを使うと好きな見た目の UI を自作できます。自分は Raycast 風のダークテーマのトースト通知を実装して、hs.alert の代わりに使っています。
このトーストはモジュール化して、Hammerspoon 経由の通知は全てこの見た目で表示されるようにしています。ちなみに macOS 標準の通知も使えますが、画像コピー時の挙動や、通知が全て Hammerspoon のアイコンになってしまうのが嫌でこの方法にしています。
コード
local function showToast(title, message)
local screen = hs.screen.mainScreen()
local sf = screen:frame()
local w, h = 320, 56
local cv = hs.canvas.new({
x = sf.x + sf.w - w - 16,
y = sf.y + sf.h - h - 16,
w = w, h = h
})
cv[1] = {
type = "rectangle", action = "fill",
roundedRectRadii = { xRadius = 10, yRadius = 10 },
fillColor = { red = 0.12, green = 0.12, blue = 0.13, alpha = 0.95 },
}
cv[2] = {
type = "text", text = title,
textColor = { white = 1, alpha = 0.9 },
textSize = 13, textFont = ".AppleSystemUIFontBold",
frame = { x = 12, y = 8, w = w - 24, h = 20 },
}
cv[3] = {
type = "text", text = message,
textColor = { white = 1, alpha = 0.55 },
textSize = 12,
frame = { x = 12, y = 28, w = w - 24, h = 20 },
}
cv:show()
hs.timer.doAfter(3, function() cv:delete() end)
end
Option キーダブルタップでスクリーンショット
⌘⇧4 はよく使う割に片手で押しづらいので、Option キーをダブルタップすると範囲選択スクリーンショットが起動するようにしています。
また、⌘ は修飾キーとしてアプリやウィンドウの中の操作に対する割り当て、Option はグローバルなものを割り当てたいという思想があり、⌘⇧4 はこれに反していたので変えたかったという癖の強い事情もあります。
hs.eventtap.checkKeyboardModifiers() をポーリングして Option キーの押下・リリースを検知します。他の修飾キーが同時に押されていたり、Option を押しながら別のキーを打った場合は発火しません。
コード
local lastTapTime = 0
local doubleTapDelay = 0.3
local wasAltPressed = false
hs.timer.doEvery(0.02, function()
local mods = hs.eventtap.checkKeyboardModifiers()
local altNow = mods.alt == true
local otherMod = mods.cmd or mods.ctrl or mods.shift
if not altNow and wasAltPressed and not otherMod then
local now = hs.timer.secondsSinceEpoch()
if now - lastTapTime < doubleTapDelay then
hs.eventtap.keyStroke({"cmd", "shift"}, "4")
lastTapTime = 0
else
lastTapTime = now
end
end
wasAltPressed = altNow
end)
Sidecar の解像度チューザー
iPad を Mac のサブディスプレイとして使える Sidecar は、外で作業をすることの多い筆者にとってとても便利な機能です。一方で、標準では Sidecar の解像度の変更ができません。
これも Hammerspoon を使うと実現可能です。Ctrl+Opt+Cmd+D で hs.chooser の一覧から選べるようにしました。hs.screen.availableModes() で取得できる解像度一覧を chooser に渡すだけです。

コード
local function showResolutionChooser()
local sidecar = findSidecarDisplay()
local modes = sidecar:availableModes()
local current = sidecar:currentMode()
local choices = {}
for _, mode in ipairs(modes) do
local isCurrent = (mode.w == current.w and mode.h == current.h)
table.insert(choices, {
text = string.format("%d×%d%s", mode.w, mode.h, mode.scale == 2 and " Retina" or ""),
subText = string.format("%dHz%s", mode.freq, isCurrent and " • CURRENT" or ""),
mode = mode,
image = isCurrent and hs.image.imageFromName("NSMenuOnStateTemplate") or nil
})
end
local chooser = hs.chooser.new(function(choice)
if choice then
sidecar:setMode(choice.mode.w, choice.mode.h, choice.mode.scale)
end
end)
chooser:choices(choices)
chooser:show()
end
hs.hotkey.bind({"ctrl", "alt", "cmd"}, "d", showResolutionChooser)
AeroSpace のワークスペース切替 UI
AeroSpace を使っていて、ワークスペースの切替を Cmd+Tab 風のグリッド UI でできるようにしています。Alt+` で起動すると、各ワークスペースにいるアプリのアイコンがパネルに並びます。

hs.canvas で UI を描画し、アプリアイコンは hs.image.imageFromAppBundle() で取得します。ワークスペースのデータは AeroSpace の CLI を hs.task で非同期に叩いて取得しています。
コード
-- aerospace CLI を非同期で叩く
local task = hs.task.new("/opt/homebrew/bin/aerospace", function(_, stdout)
local wsList = hs.json.decode(stdout)
-- ワークスペースごとのウィンドウ情報を取得して描画
callback(wsList)
end, { "list-workspaces", "--all", "--json" })
task:start()
まとめ
Hammerspoon は、既存のツールで「あとちょっと」が足りないと感じている方に特におすすめです。Lua を書いたことがなくても Claude Code が書いてくれるので、インストールさえしておけば「こんなのできないかな?」と思った時にとりあえず Claude Code にお願いできます。
この他にも合わせて 10 以上のモジュールを作っているので、今回の記事が好評であれば第二弾を書こうと思います。
Discussion