ClaudeCode の hooks 機能で、エージェントの状態(thinking, notification, done など)を tmux のウィンドウ名やスタイルに反映するscriptを自作してる。ホスト上では問題なく動くんだがmacOS の Docker環境で devcontainer 内から ClaudeCode を実行すると hooks が silent no-op になり、tmux ステータスが一切更新されなくなっていた。
macOS の Docker 環境では、bind mount した unixdomainソケットにコンテナ内から接続できないらしい。VirtioFSのファイル共有はソケットは見せるがkernelレベルのソケット通信はVM境界を越えさせないようだった。
$ ls -la /private/tmp/tmux-501/default
srwxrwx--- 1 ubuntu ubuntu 0 Mar 26 18:39 default # ファイルは見える
$ tmux display-message -p "#{window_id}"
no server running on /private/tmp/tmux-501/default # 接続は失敗
ファイルとしては見えるのにソケット通信が通らない。WSL(ubuntu)ではこの問題は発生しない(VM境界がなく、直接ソケットアクセスが通るって話)。
解決策
ホスト側でTCPリレー
unixdomainソケットが VM境界を越えられないということなので、ホスト側で socat で TCP リスナーを立て、hook の実行をリレーしつつホストに委譲。
コンテナ内で tmux コマンドを実行するのではなく、コンテナからはアクションとペイン ID だけを TCP で送信し、ホスト側の socat がそれを受けて hook スクリプトをホスト上で直接実行する。
ホスト側(devcontainer.jsonのinitializeCommandで起動):
SOCK=${TMUX%%,*}
command -v socat >/dev/null 2>&1 && [ -S "$SOCK" ] && {
pkill -f 'socat.*TCP-LISTEN:2489' 2>/dev/null
socat TCP-LISTEN:2489,bind=127.0.0.1,reuseaddr,fork \
SYSTEM:'read action pane; TMUX_PANE=$pane bash ~/.claude/hooks/tmux-window-claude-status.sh $action' &
} || true
socat の SYSTEM アドレスにより、TCP 接続ごとに hook をホスト上で fork 実行する。read action pane で TCP から送られた 1 行を分割し、TMUX_PANE を設定してから hook を呼ぶ。
hook スクリプト側でフォールバック
コンテナ内の bash /dev/tcp 疑似デバイスで TCP を直接送信するため、コンテナ側に socat は不要。
command -v tmux &>/dev/null || exit 0
action="$1"
pane="$TMUX_PANE"
# Devcontainer内(=ソケット到達不可)ならTCPリレーへフォールバック
if ! tmux display-message -p '' 2>/dev/null; then
[ "$DEVCONTAINER" = "true" ] && \
{ echo "$action $pane" >/dev/tcp/host.docker.internal/2489; } 2>/dev/null
exit 0
fi
まとめ
| 環境 | 直接ソケット | 動作 |
|---|---|---|
| ホスト (macOS/WSL) | 接続可 | 従来通り直接 tmux 操作 |
| devcontainer on WSL | 接続可 | unixdomainソケットをmountし直接使用 |
| devcontainer on macOS | 接続不可 | TCP リレーでホストに委譲 |
おわり
ファイルは見えるのにソケットが通らないという問題を、ubuntuかmacOSかという問題に間違って当てはめてしまい特定に時間がかかった。。
socat TCP リレーは力技感あるがコンテナ側の変更が最小で済むのはよい、軽量化を諦めなくて済む。hookはソケットが繋がるならそのまま使う/繋がらないなら TCP で投げる、というシンプルな分岐だけでいける。
コードはこちら