Dev Container で起動した Claude Code から Hooks でホストの Mac に通知する
masaki です。
弊社では生成 AI を業務に積極的に取り入れています。
これまでにも以下のような記事を公開してきました。
最近はコードアシストにとどまらず、エージェント型 AI ツールを活用する動きが活発になっています。
今回は、よく話題にもなり弊社でも利用している Claude Code について、少し便利になる通知設定のテクニックをご紹介します。
はじめに
Claude Code を使っていると、長いタスクを実行している間に他の作業を始めてしまい、処理が終わっているのに長時間放置していた...なんてことありませんか?
そんな時に便利なのが、最近追加された Hooks 機能を使った通知です。
色々な記事で紹介されているように、私も Mac で terminal-notifier
というツールを使って通知するようにしました。
ただ、ここで一つ問題があります。弊社では開発環境に Dev Container を採用しているのですが、コンテナ環境からはホストの Mac の通知機能に直接アクセスできないため、そのままでは Claude Code の Hooks を使った通知ができません。
とはいえ、Dev Container のようなコンテナ環境でコーディングエージェントを動かすことには、セキュリティ面で大きなメリットがあるので、できれば Dev Container 上で Claude Code を利用したいです。
※ Claude Code の公式ドキュメントでも、コンテナ上でコーディングエージェントを実行することについて言及されています
そこで今回は、Dev Container 内の Claude Code から、ホストの Mac に通知を送る仕組みを作ってみました。
概要
以下に全体図を示します。
仕組みとしては以下のとおりです。
- Mac 側で簡易的な HTTP サーバーを立ち上げる
- Docker コンテナから
host.docker.internal
経由で 1 にアクセスする - 1 の HTTP サーバーが Mac の通知システムを通じて通知する
host.docker.internal とは?
host.docker.internal
は、Docker Desktop が提供する特殊な DNS 名で、コンテナ内からホストマシンの IP アドレスを参照できます。
これにより、コンテナ内のアプリケーションがホストで動作しているサービスにアクセスすることが可能になります。
詳しくは以下の公式ドキュメントを参照してください。
Networking | Docker Docs
それではステップバイステップで実装していきます。
環境
使用したツールの各バージョンは以下のとおりです。
ツール | バージョン | インストール環境 |
---|---|---|
macOS | Sequoia | Mac |
Docker Desktop | 4.43.1 | Mac |
Dev Container | 0.422.0 | Mac |
terminal-notifier | 2.0.0 | Mac |
Claude Code | 1.0.51 | コンテナ |
Ruby | 3.3.8 | Mac |
最小構成を作る
まずは最小限の実装で動作確認をしていきます。
Step 1: terminal-notifier のインストール(Mac)
brew install terminal-notifier
インストール後、動作確認をしておきましょう。
terminal-notifier -message "テスト通知"
通知が表示されれば準備完了です。
Step 2: HTTP サーバーを作る
ホストで立ち上げる通知サーバーを作成します。
HTTP サーバーであれば何でも構いませんが、本記事では手軽に実装できる Ruby と Sinatra を使います。
notification_server.rb
を作成します。
require 'sinatra'
# HostAuthorization を明示的に許可ホストに追加
# これがないと host.docker.internal からのアクセスが拒否される
set :host_authorization, {
permitted_hosts: [
'host.docker.internal', # Docker Desktop のホスト名
'localhost' # ローカルからのテスト用
]
}
# 通知エンドポイント
get '/notify' do
system("terminal-notifier -message \"#{params[:message]}\" -title \"Claude Code\"")
end
必要な Ruby gem をインストールします。
gem install sinatra
サーバーを起動します:
ruby notification_server.rb
別のターミナルで動作確認:
# 通知テスト
curl "http://localhost:4567/notify?message=サーバーが動作しています"
Step 3: Hooks の設定(コンテナ)
次に Claude Code の Hooks を設定します。
コンテナ内でプロジェクトのルートディレクトリに移動し、.claude/settings.local.json
に以下の設定を追加します。
{
"hooks": {
"Notification": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "curl 'http://host.docker.internal:4567/notify?message=ユーザーのアクションが必要です'"
}
]
}
],
"Stop": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "curl 'http://host.docker.internal:4567/notify?message=タスクが完了しました'"
}
]
}
]
}
}
設定ファイルについて
Claude Code では 2 種類の設定ファイルを使用できます。
-
.claude/settings.json
: プロジェクト全体で共有される設定 -
.claude/settings.local.json
: 個人用の設定(gitignore に追加推奨)
今回は個人的な通知設定なので、.claude/settings.local.json
を使用しています。
詳細は公式ドキュメントを参照してください。
Step 4: 動作確認
準備が整ったので、実際に動作確認してみましょう。
- Mac 側で
notification_server.rb
を起動 - Dev Container を起動
- コンテナ内で Claude Code を実行
# Dev Container 内で
claude -p "pwd コマンドを実行してください"
タスク完了時、以下のように通知が表示されたら成功です 🎉
トラブルシューティング
通知が届かない場合
- コンテナから接続できるか確認
# コンテナ内から
curl "http://host.docker.internal:4567/notify?message=サーバーが動作しています"
- macOS の通知設定を確認
- システム設定 > 通知 >
terminal-notifier
が許可されているか
- ファイアウォールの設定
- macOS のファイアウォールが 4567 ポートをブロックしていないか
改善編:より実用的な通知システムへ
基本的な実装ができたので、実際の開発で使いやすくするための改善を行いました。
大きく変更したところは、「Ruby スクリプトを実行するためのラッパースクリプト作成」です。
ただし、2つのスクリプトを管理することは煩雑になるため、ラッパースクリプト内に Ruby スクリプトを含めました。
またスクリプトにはサーバー起動や停止などのコマンドも準備しました。
これにより、設定や通知を簡単なコマンドで実行できるようにしました。
以下が改善したスクリプトです。
#!/usr/bin/env bash
case "$1" in
start)
# Ruby 一時ファイルを作成
temp_ruby_file=$(mktemp -t any_notifier_server.xxxxxx)
# bin/any_notifier_server の内容を一時ファイルに書き込み
cat > "$temp_ruby_file" << 'EOF'
#!/usr/bin/env ruby
begin
require 'sinatra'
rescue LoadError
system('gem install sinatra')
Gem.clear_paths
require 'sinatra'
end
# HostAuthorization を明示的に許可ホストに追加
set :host_authorization, {
permitted_hosts: [
'host.docker.internal', # Docker Desktop のホスト名
'localhost' # ローカル用
]
}
get '/notify' do
title = params[:title]
subtitle = params[:subtitle]
message = params[:message]
app = params[:app]
sound = params[:sound]
system("terminal-notifier -message '#{message}' -title '#{title}' -subtitle '#{subtitle}' -execute 'open -a \"#{app}\"' -sound '#{sound}'")
end
# プロセス終了時のクリーンアップ
Signal.trap('INT') do
File.delete(__FILE__) if File.exist?(__FILE__)
exit
end
Signal.trap('TERM') do
File.delete(__FILE__) if File.exist?(__FILE__)
exit
end
EOF
# 一時ファイルを実行してサーバー起動
chmod +x "$temp_ruby_file"
nohup ruby "$temp_ruby_file" -p 5678 > /dev/null 2>&1 &
echo "Any notifier server started"
;;
stop)
pids=$(lsof -t -i:5678 2>/dev/null)
if [ -n "$pids" ]; then
echo "$pids" | xargs kill
echo "Any notifier server stopped"
else
echo "No any notifier server running on port 5678"
fi
;;
send)
# パラメータを解析
message=""
title="Claude Code"
subtitle=$(basename $(pwd))
app="iTerm"
sound="Default"
shift # "send" を削除
while [[ $# -gt 0 ]]; do
case $1 in
-m|--message)
message="$2"
shift 2
;;
-t|--title)
title="$2"
shift 2
;;
-s|--subtitle)
subtitle="$2"
shift 2
;;
-a|--app)
app="$2"
shift 2
;;
--sound)
sound="$2"
shift 2
;;
*)
if [ -z "$message" ]; then
message="$1"
fi
shift
;;
esac
done
if [ -z "$message" ]; then
echo "Error: No message provided for send command"
echo "Usage: $0 send -m <message> [-t <title>] [-s <subtitle>] [-a <app>]"
exit 1
fi
# URLエンコード
message=$(printf '%s' "$message" | jq -sRr @uri)
title=$(printf '%s' "$title" | jq -sRr @uri)
subtitle=$(printf '%s' "$subtitle" | jq -sRr @uri)
app=$(printf '%s' "$app" | jq -sRr @uri)
sound=$(printf '%s' "$sound" | jq -sRr @uri)
if [ -f "/.dockerenv" ]; then
host="host.docker.internal"
else
host="localhost"
# コンテナ環境でない場合、サーバーが起動しているかチェック
if ! lsof -i:5678 >/dev/null 2>&1; then
echo "Any notifier server not running. Starting server..."
if "$0" start; then
echo "Any notifier server started"
# サーバー起動を少し待つ
sleep 3
else
echo "Error: Failed to start any notifier server"
exit 1
fi
fi
fi
curl -s "http://$host:5678/notify?message=$message&title=$title&subtitle=$subtitle&app=$app&sound=$sound"
;;
setup-container)
# container-id の検証
if [ -z "$2" ]; then
echo "Error: Container ID required"
echo "Usage: $0 setup-container <container-id>"
exit 1
fi
container_id="$2"
# コンテナの存在確認
if ! docker ps -q --filter "id=$container_id" | grep -q .; then
echo "Error: Container $container_id not found or not running"
exit 1
fi
# 一時ファイルを作成して any-notifier スクリプトの内容を書き込み
temp_script=$(mktemp -t any_notifier_setup.XXXXXX)
# 現在のスクリプト内容を一時ファイルに出力
cat "$0" > "$temp_script"
# パーミッションを付与
chmod +x "$temp_script"
# コンテナ内に配置
if docker cp "$temp_script" "$container_id:/tmp/any-notifier" && \
docker exec "$container_id" mv /tmp/any-notifier /usr/local/bin/any-notifier && \
docker exec "$container_id" chmod +x /usr/local/bin/any-notifier; then
echo "Any notifier script installed in container $container_id"
# ホスト側でany-notifierサーバーを起動
echo "Starting any notifier server on host..."
if "$0" start; then
echo "Any notifier server started on host"
else
echo "Warning: Failed to start any notifier server on host"
fi
else
echo "Error: Failed to install any notifier script in container $container_id"
rm -f "$temp_script"
exit 1
fi
# 一時ファイルをクリーンアップ
rm -f "$temp_script"
;;
*)
echo "Usage: $0 {start|stop|send -m <message> [-t <title>] [-s <subtitle>] [-a <app>] [--sound] |setup-container <container-id>}"
exit 1
;;
esac
設定方法
上記コードをコピーし、 Mac 上の usr/local/bin
配下などに any-notifier
として配置し、スクリプトとして実行できるよう実行権限を変更してください。
chmod +x any-notifier
Dev Container を使用しているプロジェクトの .claude/settings.local.json
を変更します。
{
"hooks": {
"Notification": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "any-notifier send 'ユーザーのアクションが必要です'"
}
]
}
],
"Stop": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "any-notifier send 'タスクが完了しました'"
}
]
}
]
}
}
Dev Container を立ち上げたら、コンテナ内にスクリプトをセットアップ(コピー)する以下のコマンドを実行します。(コマンド実行時、Mac 側で Ruby サーバーが起動します)
any-notifier setup-container {コンテナID}
本スクリプトでは URL クエリに空白文字が入っていることによるエラーを避けるため、jq
コマンドで URL エンコーディングを行っています。
そのため、コンテナ環境に jq
をインストールする必要があります。
Dev Container 環境に jq
をインストールし、Claude Code を立ち上げ、タスク完了時に通知が表示されたら成功です 🎉
その他、タイトルやサブタイトルや起動するアプリをオプションによって変更可能にしているので、必要に応じてカスタマイズしてください。
any-notifier send --message "タスクが完了しました" \
--title "Claude Code" \
--subtitle "コンテナ環境" \
--sound Ping \
--app "iTerm" # 通知をクリックしたときに起動するアプリ
まとめ
本記事では、Claude Code の Hooks を使い、Mac + Dev Container 環境で通知を実現する方法を紹介しました。
ポイント:
-
host.docker.internal
を使用してコンテナからホストへアクセス - 簡易HTTPサーバーで通知を中継
- 単一スクリプトで管理を簡素化
セキュリティと利便性のバランスを取りながら、快適な開発環境を構築していきましょう!
ご質問やフィードバックがありましたら、ぜひコメントでお知らせください。
Discussion