筆者はMacアプリ「Offrun」を開発しています。この記事の設定は、Claude CodeとCodexの標準機能(hooks)とmacOSに最初から入っているコマンドだけで動きます。Offrunの紹介は末尾に短く書きました。
何に困るのか
git worktreeでエージェントごとに作業フォルダを分けると、Claude CodeとCodexを同じリポジトリで並列に動かせます。ただ、3つ、4つと増えてくると、次のことが分からなくなります。
- どのエージェントが返事を終えたのか
- どのエージェントが、コマンドの承認を待って止まっているのか
承認待ちに気づかないと、そのエージェントは何分でも止まったままです。git status を見れば何が変わったかは分かりますが、エージェントがまだ動いているのか、こちらを待っているのかは分かりません。
この記事では、Claude CodeとCodexの両方のhooksから同じスクリプトを1つ呼び、「どのworktreeで、どのエージェントが、終わったのか、承認を待っているのか」をMacの通知に出します。
確認した環境: macOS 26.4.1、Claude Code 2.1.300、Codex CLI 0.162.1、jq 1.7.1(macOSに標準で入っています)
使うイベント
| 知りたいこと | Claude Code | Codex |
|---|---|---|
| 返事が終わった | Stop |
Stop |
| コマンドの承認を待っている |
Notification(permission_prompt) |
PermissionRequest |
| 返事のあと、入力を待ったまま約60秒 |
Notification(idle_prompt) |
なし |
どちらのCLIも、hookのコマンドに標準入力でJSONを渡します。その中に、イベント名の hook_event_name と、エージェントが動いているフォルダの cwd が入っています。worktreeごとにフォルダが違うので、cwd を見ればどのworktreeの話かが分かります。
実際にフックへ渡ったJSONから、関係する項目だけを抜き出すとこうなりました(session_id や transcript_path などは省いています)。
{"hook_event_name":"Stop","cwd":"/private/tmp/qn-test/app-login"}
{"hook_event_name":"Notification","notification_type":"permission_prompt","message":"Claude needs your permission","cwd":"/private/tmp/qn-test/app-login"}
{"hook_event_name":"Notification","notification_type":"idle_prompt","message":"Claude is waiting for your input","cwd":"/private/tmp/qn-test/app-login"}
{"hook_event_name":"PermissionRequest","tool_name":"Bash","tool_input":{"command":"touch hello.txt","description":"May I create hello.txt in this folder? The filesystem sandbox is read-only."},"cwd":"/private/tmp/qn-test/app-api"}
上の3行がClaude Code(app-login というworktree)、最後の行がCodex(app-api というworktree)です。
1. 通知を出すスクリプト
#!/bin/bash
# Claude Code / Codex の hooks から呼ばれ、どのworktreeの何が起きたかをMacに通知する
agent=${1:-agent}
input=$(cat)
event=$(jq -r '.hook_event_name // empty' <<<"$input")
kind=$(jq -r '.notification_type // empty' <<<"$input")
cwd=$(jq -r '.cwd // empty' <<<"$input")
case "$event:$kind" in
Stop:*) state="終わりました" ;;
Notification:permission_prompt|PermissionRequest:*) state="承認を待っています" ;;
Notification:idle_prompt) state="入力を待っています" ;;
*) exit 0 ;;
esac
where=$(basename "$cwd")
branch=$(git -C "$cwd" branch --show-current 2>/dev/null)
osascript -e 'on run argv' \
-e 'display notification (item 2 of argv) with title (item 1 of argv) sound name "Glass"' \
-e 'end run' "$agent: $where" "${branch:+[$branch] }$state"
mkdir -p ~/bin
# 上の内容を ~/bin/agent-notify.sh に保存してから
chmod +x ~/bin/agent-notify.sh
ポイントは3つです。
-
第1引数はエージェント名です。 Claude Codeからは
claude、Codexからはcodexを渡すので、通知のタイトルが「claude: app-login」「codex: app-api」のようになります。本文にはブランチ名と状態が「[login] 承認を待っています」のように入ります。 - 文字列はAppleScriptに引数で渡しています。 スクリプトの中に直接埋め込むと、フォルダ名やブランチ名に引用符が入ったときに壊れるためです。
- 何も出力せずに終わります。 CodexのPermissionRequestは、フックが許可か拒否かを返すとそれに従いますが、何も返さなければいつもの承認画面が出ます。このスクリプトは知らせるだけで、承認はこれまでどおり自分で行います。
hooksに入れる前に、手で試せます。
echo '{"hook_event_name":"Stop","cwd":"'"$PWD"'"}' | ~/bin/agent-notify.sh test
2. Claude Codeに設定する
全プロジェクト共通にするなら ~/.claude/settings.json に書きます。すでに "hooks" がある場合は、その中に Stop と Notification を足してください。
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "~/bin/agent-notify.sh claude" }
]
}
],
"Notification": [
{
"matcher": "permission_prompt|idle_prompt",
"hooks": [
{ "type": "command", "command": "~/bin/agent-notify.sh claude" }
]
}
]
}
}
-
Stopは返事が終わるたびに呼ばれます。claude -pで動かした場合も呼ばれました。 -
Notificationは通知の種類ごとにmatcherで絞れます。permission_promptは承認画面が出て数秒たったとき、idle_promptは返事のあと約60秒入力がないときに来ます。 - 承認を聞かずに進めるモード(auto modeなど)では承認画面が出ないので、
permission_promptの通知も来ません。試したときも、auto modeではStopしか来ませんでした。
3. Codexに設定する
Codexも同じ形のJSONで書けます。全プロジェクト共通にするなら ~/.codex/hooks.json です。
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "~/bin/agent-notify.sh codex" }
]
}
],
"PermissionRequest": [
{
"hooks": [
{ "type": "command", "command": "~/bin/agent-notify.sh codex" }
]
}
]
}
}
Codexには、hooksを信頼するまで実行しない仕組みがあります。ファイルを置いてからCodexを起動すると「Hooks need review」と出るので、内容を確認して信頼してください。あとから /hooks を開いて確認し、t ですべて信頼することもできます。信頼はフックの内容ごとに記録されるので、ファイルを書き換えたら、もう一度確認が必要です。
-
PermissionRequestは承認が必要なコマンドのときだけ呼ばれます。読み取り専用のサンドボックスでファイルを作ろうとしたときに、承認画面が出るのと同時に呼ばれました。 -
Stopは対話画面でもcodex execでも呼ばれました。 - Claude Codeの
idle_promptに当たるイベントはありません。「終わったのに放置している」ことは、Stopの通知で気づくことになります。
Codexには config.toml の notify という設定もありますが、この記事ではClaude Codeと同じ形で書けて、承認待ちも拾えるhooksを使っています。
4. 使ってみて調整したくなるところ
-
通知が多すぎる場合:
Stopを外して、承認待ちと入力待ちだけにします。こちらの手が必要なときだけ鳴るようになります。 -
音を分けたい場合:
caseの中で状態ごとに音の名前を変えます(Glass、Ping、Submarineなど、/System/Library/Soundsにある名前)。 -
通知が出ない場合: osascriptの通知は「スクリプトエディタ」からの通知として扱われます。システム設定の「通知」でスクリプトエディタが許可されているか、集中モードが入っていないかを確認してください。画面をミラーリングまたは共有している間も、macOSは標準ではバナーと音を出さず、通知センターに入れるだけです。試したときはこれが原因で、システムのログに
muted by display state (displayMirrored)と出ていました(/usr/bin/log show --last 1m --predicate 'process == "NotificationCenter"'で確認できます)。ミラーリング中も出したい場合は、システム設定の「通知」にある、ミラーリングや共有中の通知を許可する項目をオンにします。
5. 通知だけでは足りないところ
この仕組みで「待っているのに気づかない」はかなり減ります。それでも、手で回していると次のところが残ります。
- 通知は来た瞬間のものです。席を外している間に何が終わったかは、通知センターをさかのぼるしかありません。
- 「いま動いているのはどれか」は分かりません。通知は状態が変わったときに1回来るだけです。
- エージェントの種類が増えるたびに、CLIごとにフックの書き方と置き場所を調べて設定する必要があります。
私はこのあたりをまとめて扱うMacアプリを作りました。次の節で簡単に紹介します。
Offrunについて
Offrunは筆者が開発しているMacアプリで、Claude Code、Codex、Antigravity CLI、Grok Build、Muse Codeを、いま使っている自分のログインのままMac上で動かします。チャットごとにgit worktreeを作り、どのエージェントが作業中か、どれが返事を待っているかを1つの画面に一覧で出すので、ターミナルを順に見に行く必要がありません。
- 無料で、カード登録は不要です。各エージェントの契約は別に必要です。
- macOS 14以降のMacが対象で、Apple SiliconとIntelの両方に対応しています。Windows、Linuxは未対応です。
- 初回起動にOffrunのアカウント(Googleまたはメールでのログイン)が必要です。
この記事の通知はOffrunなしで動くので、まずは手元の設定で試してみてください。
確認したこと、していないこと
| 内容 | 状況 |
|---|---|
Claude Codeの Stop(claude -p と対話画面)、Notification の permission_prompt と idle_prompt
|
実行し、フックに渡ったJSONを記録して確認 |
Claude Codeでauto modeのときは permission_prompt が来ない |
実行して確認 |
Codexの Stop(codex exec と対話画面)、PermissionRequest
|
実行し、フックに渡ったJSONを記録して確認 |
Codexで、リポジトリの .codex/hooks.json を /hooks で信頼すると動く |
実行して確認 |
設定ファイルの置き場所(~/.claude/settings.json、~/.codex/hooks.json) |
公式ドキュメントで確認。Claude Codeは同じ内容を --settings で渡して実行 |
フックの command で ~ が展開される |
Codexで実行して確認 |
| 通知スクリプトがworktree名とブランチ名を出す | 2つのworktreeで実行して確認 |
| 通知がスクリプトエディタから届き、通知センターに入る | システムのログで確認。画面をミラーリングしていたため、バナーと音は出ないことも確認 |
| Windows、Linux | 対象外 |
この記事はAI(Claude)で下書きし、記事中の設定とコマンドは実際に実行して結果を確認しています。