Ryuto Hara.

WATCHING, NEVER TOUCHING

Subghost for Mac

見に行かなくても、
終わったら分かる。

ターミナルの「終わった」を、
Macのノッチで。

Claude Code と Codex CLI のタスク状態を,ノッチに置きっぱなしに.
確かめに戻らなくても,完了のほうから知らせます.

仕組みを見てみる

企画・デザイン・Swift / SwiftUI実装:Ryuto Hara

Subghost待機中
Subghostclaude のタスクが完了しました
フックを受け取ってから完了を知らせるまでの流れ(このページ上での再現です)

THE COST OF CHECKING

「終わったかな」を、
何度も確かめている。

AI CLI に任せた作業は,数十秒で片づくこともあれば,十数分かかることもあります.待つでもなく,忘れるでもなく,ターミナルを何度も覗く.その往復をなくすために作りました.

確認のために、戻る。

進んでいるかを知りたいだけなのに,そのたびにウィンドウを切り替える.いま考えていたことを,一度手放すことになります.

集中すると、気づかない。

別の作業に入り込むと,終わったことに何分も気づけない.待っていないのに待たされる時間が,静かに積もります.

どれが動いているのか。

セッションが増えるほど,どのターミナルがまだ働いているのか,ひとつずつ開いてみないと分かりません.

ONE GLANCE, EVERY SESSION

ひと押しで、
いま動いている全部。

⌥ Space でセッション一覧が下りてきます.CLI,作業フォルダ,状態,最終活動時刻.知りたいことだけを,同じ並びで.

  • 並ぶのは、フックが作ったセッションだけプロセスを探し回って勝手に一覧へ足すことはしません.検出はあくまで補助で,PIDとTTYを補う役目に留めています.
  • 同じ端末の中でも、取り違えないひとつのTTY上に複数のセッションがあっても,別のものとして扱います.取り違えないことを,テストで固定しています.
  • 行からできるのは、移動だけ送信ボタンも,承認の選択肢もありません.開くのは,そのセッションが動いているターミナルだけです.
claude · ~/portfolio_siteWorking
claude~/portfolio_siteWorking12:04
codex~/Create App/subghostDone11:58
claude~/rinkou/handoutDone11:31
行からできるのは、そのターミナルへ移動することだけ⌥Space
セッション一覧の表示イメージ(このページ上での再現です)

IT READS EVENTS, NOT SCREENS

見ているのは、
画面ではない。

端末のキャプチャも,ターミナルマルチプレクサも使いません。
CLIが発するフックイベントを、同じMacの中のソケットで受け取るだけです。

  1. Claude Code / Codex CLIフックを実行する
  2. subghost-bridge/bin/sh · 上限1秒
  3. Unix domain socket~/.subghost/run
  4. SubghostHookServer → 状態更新

返すのは空の応答({})だけ。判断も入力も戻しません。

Working

指示が送られた合図。ここから Working に入り、終了イベントが来るまで保ちます。

フック設定は Claude Code なら ~/.claude/settings.json,Codex なら ~/.codex/hooks.json へ追記します.変更前にバックアップを作り,Subghost が目印を付けた項目だけを扱うので,自分で書いたフックはそのまま残ります.

TWO STATES. NO MORE.

Working と Done、
ふたつだけ。

状態を増やすほど,見た瞬間の判断は遅くなります.ノッチという狭い場所に置くものは,2つに絞りました.

受け取ったフックイベントと、そのときの表示
フックイベント表示扱い
SessionStartDoneセッションが立ち上がった直後.まだ何も始まっていない.
UserPromptSubmitWorking指示が送られた合図.ここから作業中に入る.
PreToolUse / PostToolUseWorkingツールの呼び出し前後.作業が進んでいる証拠.
SubagentStart / SubagentStopWorkingサブエージェントの稼働中も,全体としては作業中.
Notification / PermissionRequestWorking承認待ちも作業の途中.Subghost は可否を答えない.
StopDoneタスクの終了.通知とサウンドはここで鳴る.
SessionEndDone完了として扱い,60秒後に一覧から静かに消す.
StopFailureDone内部ではエラーとして記録し,2状態の表示では完了に寄せる.
時間では、完了にしない。

終了イベントを受け取れなかったときは,どれだけ待っても Working のままにします.疎通が切れていただけなのに「終わりました」と伝えるほうが,ずっと困るからです.疑わしいときのために,設定には受信テストと,CLIごとの最終受信時刻を置いています.

A LINE THAT DOES NOT MOVE

できないことを、
決めてある。

常駐して,ずっと動いているアプリです.だからこそ,踏み込まない線をはっきり引きました.

プロンプトを送らない

Subghost から CLI へ文字を送る経路そのものを持ちません.入力欄も送信ボタンも画面にありません.

承認や質問に答えない

PermissionRequest にも空の応答だけを返します.許可するかどうかの判断は,CLI 本来の画面に委ねます.

キー入力を作らない

アクセシビリティ権限を入力の合成には使いません.見えている端末へ勝手に打ち込むことはありません.

プロセスを終了しない

CLI へシグナルを送る経路を置かず,「その経路が存在しないこと」自体をテストで確かめています.

できるのは,そのターミナルへ移動することと,指定したタスクの完了後に Mac をスリープさせることだけ.

OFF BY DEFAULT

読まない、が既定。

手元の記録を扱うアプリだからこそ,初期値を安全側に置いています.

本文は、既定で読まない。

会話本文のプレビューは初期状態で無効です.この間は,フックが指し示すセッション記録を開きません.状態監視に本文は要らないからです.

戻せば、履歴からも消える。

プレビューを無効へ戻した時点で,保存済み履歴の本文はプレースホルダへ置き換わり,Subghost からは復元できなくなります.

外へは、出ない。

通信は同じ Mac の中の Unix ドメインソケットだけ.会話も履歴も診断情報も,インターネットへ送る機能を持ちません.

THOUGHTFUL, INSIDE AND OUT

そばに置き続けるものは、
邪魔をしないことが先。

01 / RESILIENCE

落ちていても、CLIを止めない。

ソケットが無ければブリッジは何もせず正常終了し,応答が遅いときも1秒で切り上げます.Subghost の不調が,CLI の作業を待たせる理由になってはいけません.

[ -S "$SOCK" ] || exit 0 curl -s -m "$TIMEOUT" --unix-socket "$SOCK" …
02 / PRECISION

どの端末かを、祖先まで遡って。

フックは /bin/sh -c 経由で呼ばれ,その中間シェルは制御端末を持ちません(tty が ?? になる).親を1段見るだけでは特定できないため,10段まで祖先をたどり,最初に制御端末を持つプロセスを CLI 本体とみなします.

while [ "$cur" -gt 1 ] && [ "$depth" -lt 10 ]; do ps -o ppid=,tty= -p "$cur" # tty が "??" なら親へ done
03 / RECOVERY

見失っても、戻れる。

外部ディスプレイやノッチのない Mac でも困らないよう,メニューバーから一覧・設定・終了へ行けます.通知の許可も,オンボーディングか設定で選ばれたときだけ求めます.

SwiftUI · macOS 14+ · UserNotifications

確かめに戻る時間を、
手放すために。

Subghost の設計と実装を,ソースコードで公開しています.

GitHubでソースを見る ほかのアプリも見る →