結論
-
Option+1/2/4でClaude Codeデスクトップ(Cowork)を1/2/4分割に瞬時配置できる - 仕組みは Hammerspoon + AppleScript +
open -nで 80行程度のシェル - 並走しすぎで暴走するので、SessionStart hookで 4チャネルを超えたら起動拒否 する制御も同時に入れる
このセットアップで、Coworkを「左でリサーチ・右で実装・下でレビュー・もう1つでメモ」という4並列ワークフローが片手のショートカットで開ける。
完成形
┌─────────────────┬─────────────────┐
│ Claude #1 │ Claude #2 │
│ リサーチ │ 実装 │
├─────────────────┼─────────────────┤
│ Claude #3 │ Claude #4 │
│ レビュー │ メモ │
└─────────────────┴─────────────────┘
Option+4 一発でこの配置になる。Option+2 なら左右半分、Option+1 でフルスクリーン。
なぜ作ったか
CoworkはCmd+Nで新規ウィンドウを開けるが、配置は手動。並列でAIに作業させたい人間にとって、毎回ウィンドウサイズを揃えるのが地味に苦痛。
しかも放置すると「気づいたらバックグラウンドに10セッション残っていた」状態になる。プロセスもメモリも食う。
→ 配置の自動化 と 同時稼働数の上限 の両方が必要。
構成
| 役割 | ツール | 設定ファイル |
|---|---|---|
| ホットキー受け | Hammerspoon | ~/.hammerspoon/init.lua |
| ウィンドウ配置 | bash + AppleScript | ~/.claude/scripts/claude_split.sh |
| 並走チャネル制御 | Claude Code SessionStart hook | ~/.claude/hooks/check-max-channels.sh |
| 終了時の掃除 | Claude Code SessionEnd hook | ~/.claude/hooks/cleanup-channel.sh |
| タイル型WMとの共存 | AeroSpace(任意) | ~/.aerospace.toml |
実装1: Hammerspoonでホットキー登録
~/.hammerspoon/init.lua:
-- ===== Claude.app を複数起動 =====
hs.hotkey.bind({"cmd", "ctrl", "alt", "shift"}, "C", function()
hs.execute("/usr/bin/open -n /Applications/Claude.app")
hs.alert.show("Claude 起動", 0.5)
end)
-- ===== Claude 分割配置 (Option + 数字) =====
local function splitClaude(n)
local home = os.getenv("HOME")
hs.execute(string.format("/bin/bash %s/.claude/scripts/claude_split.sh %d", home, n))
hs.alert.show("Claude " .. n .. "分割", 0.5)
end
hs.hotkey.bind({"alt"}, "1", function() splitClaude(1) end)
hs.hotkey.bind({"alt"}, "2", function() splitClaude(2) end)
hs.hotkey.bind({"alt"}, "4", function() splitClaude(4) end)
open -n は同じアプリの新規インスタンスを起こすコマンド。これでCoworkウィンドウを増やせる。
実装2: 配置スクリプト
~/.claude/scripts/claude_split.sh のコア部分(全体は80行):
#!/bin/bash
set -e
TARGET=${1:-2}
# 上限超過: TARGETより多いClaude本体プロセスはkill (新しい順)
PIDS=$(ps -A -o pid=,command= | awk '/Claude\.app\/Contents\/MacOS\/Claude/ && !/Helper/ {print $1}' | sort -n)
COUNT=$(echo "$PIDS" | grep -c .)
if [ "$COUNT" -gt "$TARGET" ]; then
KILL_N=$((COUNT - TARGET))
echo "$PIDS" | tail -n "$KILL_N" | xargs kill -TERM 2>/dev/null || true
sleep 2
fi
# 画面ジオメトリを正確に取得 (メニューバー/ノッチ/Dockすべて考慮)
GEOM=$(osascript -l JavaScript -e '
ObjC.import("AppKit");
const f = $.NSScreen.mainScreen.frame;
const v = $.NSScreen.mainScreen.visibleFrame;
const menuBar = f.size.height - (v.origin.y + v.size.height);
const dockBottom = v.origin.y;
[Math.round(f.size.width), Math.round(f.size.height), Math.round(menuBar), Math.round(dockBottom)].join(",")
')
# 不足分は open -n で起動
NEEDED=$((TARGET - CURRENT_WINS))
if [ $NEEDED -gt 0 ]; then
for ((i=0; i<NEEDED; i++)); do
/usr/bin/open -n /Applications/Claude.app
sleep 1
done
sleep 1.5
fi
# AppleScript で position と size を2x2グリッドに配置(4分割の場合)
# ...
ポイントは3つ:
-
画面ジオメトリの取得:
NSScreenのframeとvisibleFrameの差分でメニューバー・ノッチ・Dockの実寸を出す。固定値で組むとMacBookのノッチ世代とそれ以前で位置がズレる -
size を2回投げる: AppleScriptで一度position/sizeをセットしても、Coworkが内部リサイズで戻すケースがある。
delay 0.05で2回送ると安定 -
不足分は
open -nで起動: 既存ウィンドウが2個しかない状態でOption+4を押したら、自動で2個追加する
ハマり①: 再起動するたびにHammerspoonが死ぬ
ログアウト・再起動すると、HammerspoonもKarabiner-Elementsも常駐しない。pgrep -fl Hammerspoon で確認するとプロセスが居ない。
init.lua は無事なのに「ホットキーが効かなくなった」と感じるのはこれが原因。
対策: ログイン項目に追加する。
osascript -e 'tell application "System Events" to make new login item \
at end with properties {path:"/Applications/Hammerspoon.app", hidden:true}'
hidden:true にすると起動時にウィンドウが出ない。メニューバーには常駐する。
確認:
osascript -e 'tell application "System Events" to get the name of every login item'
# => Raycast, Clipy, AeroSpace, Karabiner-Elements, Hammerspoon
ハマり②: バックグラウンドのCoworkチャネルが暴走する
Cowork本体ウィンドウは4個までだが、内部のセッション(並走チャネル)は何個でも生まれる。気づくと10個並走、メモリ食い放題、Dockのアイコンも何が何だか分からなくなる。
公式設定では並走数上限は出ていない(maxConcurrentSessions のような項目は2026-04時点で未実装)。
→ SessionStart hookで自前ガード を入れる。
~/.claude/hooks/check-max-channels.sh:
#!/bin/bash
set -euo pipefail
MAX_CHANNELS=4
CHANNEL_DIR="${TMPDIR:-/tmp}/claude-channels"
mkdir -p "$CHANNEL_DIR" 2>/dev/null
# 5分以上前のstaleチャネル削除(Cowork再起動時の同時発火対策)
find "$CHANNEL_DIR" -name "*.json" -mmin +5 -delete 2>/dev/null || true
EXISTING=$(find "$CHANNEL_DIR" -name "*.json" 2>/dev/null | wc -l | tr -d ' ')
TOTAL=$((EXISTING + 1))
if [ "$TOTAL" -gt "$MAX_CHANNELS" ]; then
echo "❌ Coworkチャネル上限超過: ${TOTAL}/${MAX_CHANNELS}" >&2
echo " 既に ${EXISTING} チャネル稼働中。先に余分なセッションを閉じてください。" >&2
exit 2
fi
exit 0
~/.claude/settings.json の SessionStart に登録:
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/check-max-channels.sh",
"timeout": 5
}
]
}
]
exit 2 を返すとSessionStart hookは「セッション開始を拒否」する挙動になる。
ハマり③: 終了したセッションのゾンビファイルが残る
チャネル登録ファイルは $TMPDIR/claude-channels/ に書かれるが、Cowork側に「セッション終了時にこのファイルを消す」仕組みはない。放っておくとゾンビが溜まり、何もしてないのに「上限超えました」と拒否される。
→ SessionEnd hookで自動掃除。
~/.claude/hooks/cleanup-channel.sh:
#!/bin/bash
set -euo pipefail
CHANNEL_DIR="${TMPDIR:-/tmp}/claude-channels"
[ -d "$CHANNEL_DIR" ] || exit 0
CHANNEL_CWD="${CLAUDE_PROJECT_DIR:-$PWD}"
CHANNEL_HASH=$(echo "$CHANNEL_CWD" | shasum 2>/dev/null | cut -c1-8)
# 同じcwdの最古のJSONを1件削除(このセッションが登録した分)
OLDEST=$(find "$CHANNEL_DIR" -name "${CHANNEL_HASH}-*.json" -type f \
| xargs -I{} stat -f "%m %N" {} | sort -n | head -1 | awk '{print $2}')
[ -n "$OLDEST" ] && rm -f "$OLDEST"
# 10分以上前のstaleも掃除
find "$CHANNEL_DIR" -name "*.json" -mmin +10 -delete 2>/dev/null || true
exit 0
settings.jsonに SessionEnd フックとして登録すれば、終了のたびに自分の登録を消してくれる。
おまけ: AeroSpaceを使っている場合の衝突回避
AeroSpace(タイル型WM)は Option+数字 をワークスペース切替に使うのがデフォルト。Hammerspoonの Option+1/2/4 と被るので、AeroSpace側を退避する。
~/.aerospace.toml:
# alt-1/2/4 は Hammerspoon の Claude分割に譲る
# alt-1 = 'workspace 1' ← コメントアウト
# alt-2 = 'workspace 2'
# alt-4 = 'workspace 4'
# 代わりに Cmd+Option に逃がす
alt-cmd-1 = 'exec-and-forget open -a "Google Chrome"'
alt-cmd-2 = 'exec-and-forget open -a "LINE"'
alt-cmd-4 = 'workspace 4'
# Claude.appはfloating指定(Hammerspoonが配置するため)
[[on-window-detected]]
if.app-id = 'com.anthropic.claudefordesktop'
run = ['layout floating']
タイル型WMがClaudeを勝手にタイル化するとHammerspoonの配置が上書きされるので、floating 指定が大事。
"設定が戻った" 問題の切り分けフロー
ホットキーが効かない・キー配列が標準に戻った気がする、と感じたとき、9割は 常駐プロセスが落ちているだけ。
# 1. プロセス生存確認
pgrep -fl Hammerspoon
pgrep -fl karabiner
# 2. 死んでたら起動
open -a Hammerspoon
open -a Karabiner-Elements
# 3. ログイン項目に居るか確認
osascript -e 'tell application "System Events" to get the name of every login item'
# 4. 居なければ追加
osascript -e 'tell application "System Events" to make new login item \
at end with properties {path:"/Applications/Hammerspoon.app", hidden:true}'
~/.hammerspoon/init.lua や ~/.config/karabiner/karabiner.json の中身は普通残っているので、設定ファイル自体を疑う前にプロセスを見る。
4並列ワークフローの具体例
「4分割できて何が嬉しいのか?」が一番気になるところだと思う。実際に自分が回している運用を晒す。
パターンA: リサーチ→実装→レビュー→記事化
| ウィンドウ | 役割 | プロンプト例 |
|---|---|---|
| 左上 #1 | リサーチ専任 | 「○○の公式ドキュメントを読んで、要点をMarkdownで返して」 |
| 右上 #2 | 実装専任 | 「#1の結論を元に△△を実装。Pythonで書いて」 |
| 左下 #3 | レビュー専任 | 「#2のコードをセキュリティとパフォーマンス観点でレビュー」 |
| 右下 #4 | 記事化専任 | 「今回の試行錯誤をQiita記事のドラフトに」 |
人間は #1〜#4 を巡回するだけ。各セッションは独立で動くので、待ち時間が消える。1人で動かす時の体感生産性は3〜4倍。
パターンB: A/Bテスト型
同じタスクを別アプローチでやらせて比較する。
- #1: 「Streamlitで実装して」
- #2: 「Flask+HTMXで実装して」
- #3: 「Next.jsで実装して」
- #4: 結果比較・判断
技術選定の意思決定が一気に進む。「Aの方が良さそう」を実コードで判断できる。
パターンC: 長時間バッチ + 対話
- #1〜#3: 長時間処理(データ整形・スクレイピング・大量ドキュメント要約)を投げて放置
- #4: 自分が対話しながら作業
放置組が終わるまでの間、対話組で別作業。CPUとAPI課金を遊ばせない。
トラブルシューティング詳細
実際にハマって時間を溶かしたパターンを書いておく。
Q1: Hammerspoonのホットキーが反応しない
# まずプロセス確認
pgrep -fl Hammerspoon
# 居なければ起動
open -a Hammerspoon
# init.luaが読み込まれているか確認(Hammerspoonコンソールで)
hs.reload()
メニューバーのHammerspoonアイコン → Console を開いて、エラーが出ていないか見る。init.lua のシンタックスエラーは pop-up で出ないので、コンソール確認が必須。
Q2: AppleScriptで「アシスティブデバイスへのアクセス」エラー
osascript: script error: System Events got an error: ...
→ システム設定 → プライバシーとセキュリティ → アクセシビリティ で Hammerspoon と osascript にチェックが入っているか確認。Macアップデートで権限がリセットされることがある。
Q3: SessionStart hookで exit 2 してもセッションが起動する
Cowork のバージョンによっては exit 2 の挙動が変わる可能性がある。確実に止めるなら exit 1 も試す。
# settings.json のhookでBlocking挙動を確認
cat ~/.claude/settings.json | python3 -c "import json,sys; print(json.load(sys.stdin)['hooks']['SessionStart'])"
Q4: チャネルファイルの場所が違う
$TMPDIR はサンドボックスの有無で変わる。実体はだいたいここ:
# ユーザー固有の一時ディレクトリ
echo "/var/folders/$(stat -f %i / | cut -c1-2)/$(getconf DARWIN_USER_CACHE_DIR | sed 's|.*/\([^/]*\)/C/.*|\1|')/T"
# または素直に
getconf DARWIN_USER_TEMP_DIR
getconf DARWIN_USER_TEMP_DIR で確実に取れる。スクリプトでハードコードするときはこれを使う。
応用編: SessionEnd hookで他にできること
SessionEnd フックはセッション終了時に1回呼ばれる。チャネル掃除以外にも色々使える。
1. セッション終了時にgit autosave
#!/bin/bash
# ~/.claude/hooks/auto-commit.sh
cd "$CLAUDE_PROJECT_DIR" 2>/dev/null || exit 0
if git diff --quiet 2>/dev/null && git diff --cached --quiet 2>/dev/null; then
exit 0 # 変更なし
fi
git add -A
git commit -m "WIP: claude session end $(date +%Y-%m-%d_%H:%M)" --allow-empty 2>/dev/null
「セッション中に編集したけどコミット忘れた」を物理的に防げる。
2. セッション要約をMarkdownに残す
#!/bin/bash
# ~/.claude/hooks/session-log.sh
LOG_DIR="$HOME/.claude/session-logs"
mkdir -p "$LOG_DIR"
{
echo "## $(date '+%Y-%m-%d %H:%M') セッション終了"
echo "- cwd: $CLAUDE_PROJECT_DIR"
echo "- 変更ファイル:"
cd "$CLAUDE_PROJECT_DIR" 2>/dev/null && git diff --name-only HEAD 2>/dev/null | sed 's/^/ - /'
echo ""
} >> "$LOG_DIR/$(date +%Y-%m).md"
月単位で「いつ何を触ったか」が残る。月次振り返りに使える。
3. macOS通知でセッション終了を知らせる
#!/bin/bash
osascript -e "display notification \"セッション終了 ($(basename "$CLAUDE_PROJECT_DIR"))\" with title \"Cowork\""
長時間バッチをCowork で回しているとき、終わりを見逃さない。
ログイン項目を一括追加するスクリプト
毎回 osascript を打つのが面倒なので、まとめて追加するワンライナー。
#!/bin/bash
# ~/.claude/scripts/setup-login-items.sh
APPS=(
"/Applications/Karabiner-Elements.app"
"/Applications/Hammerspoon.app"
"/Applications/AeroSpace.app"
"/Applications/Raycast.app"
"/Applications/Clipy.app"
)
for app in "${APPS[@]}"; do
name=$(basename "$app" .app)
exists=$(osascript -e "tell application \"System Events\" to get the name of every login item" \
| tr ',' '\n' | sed 's/^ *//' | grep -Fx "$name" | head -1)
if [ -z "$exists" ]; then
osascript -e "tell application \"System Events\" to make new login item at end \
with properties {path:\"$app\", hidden:true}" >/dev/null
echo "✅ 追加: $name"
else
echo "⏭ 既存: $name"
fi
done
新しいmacにセットアップする時、これ1本流せば常駐ツールが揃う。
ディスプレイ別の挙動と注意点
claude_split.sh は NSScreen.mainScreen を見るので、メインディスプレイにだけ配置される。マルチモニター環境で「サブディスプレイに4分割したい」場合は、mainScreen を screens()[1] に変える。
// JavaScript for Automation
const screens = $.NSScreen.screens;
const target = screens.objectAtIndex(1); // 2番目のディスプレイ
const v = target.visibleFrame;
13インチMBP(2560×1664)で4分割すると1ペイン1280×768程度になる。Coworkのサイドバーがあると本文領域は狭くなるので、各ペインでサイドバーを畳む(Cmd+B 等)と作業しやすい。
外部4Kモニター(3840×2160)なら4分割で1920×1040、これが一番快適。
4並列で動かす時のコスト感
Coworkは1セッションあたり一定のContext Windowを持つので、4セッション同時に動かすと単純計算でAPI呼び出し量も4倍。Maxプランなら気にしなくて良い範囲だが、従量課金で動かすと月のトークン消費は急増する。
ざっくり目安:
- 1セッション軽め利用: 月200万トークン(Sonnet基準で約30ドル)
- 4セッション並列で本気利用: 月800万トークン(同 約120ドル)
Maxプラン(月100ドル定額)の方が圧倒的に得。並列前提で使うならMax固定で問題ない。
まとめ
今日からやること
-
今日: Hammerspoonをインストールして
init.luaに Option+1/2/4 を仕込む(10分) -
今週:
claude_split.shを~/.claude/scripts/に配置 → 動作確認 → ログイン項目に追加(30分) - 今月: SessionStart/SessionEnd hookでチャネル管理を仕組み化(応用編まで含めて1時間)
この記事のキモ
- Coworkを4分割で使うには Hammerspoon + 80行のbashで十分
- 再起動で死ぬツールはログイン項目に
hidden:trueで追加 - 並走チャネルが暴走するなら SessionStart hookで
exit 2 - ゾンビファイルは SessionEnd hookで自動掃除
- macOSの一時ディレクトリは
getconf DARWIN_USER_TEMP_DIRで確実に取れる
体感、Coworkを4並列で動かせるようになると思考の解像度が変わる。並列でAIに別観点の作業をさせて、自分は司令塔に徹するスタイルが取れる。
「AIを使う人」から「AIに作業を分担させる人」への移行は、この手のオペレーション設計から始まる気がしている。
参考: